diff --git a/apps/nestjs-backend/src/types/i18n.generated.ts b/apps/nestjs-backend/src/types/i18n.generated.ts index 19f25c45b2..d661ff920f 100644 --- a/apps/nestjs-backend/src/types/i18n.generated.ts +++ b/apps/nestjs-backend/src/types/i18n.generated.ts @@ -2893,6 +2893,10 @@ export type I18nTranslations = { "noFocus": string; "noPermission": string; }; + "pasteError": { + "noPermission": string; + }; + "requiredFieldsMissing": string; "clearConfirmTitle": string; "clearConfirmDescription": string; "deleteRecordConfirmTitle": string; diff --git a/apps/nextjs-app/src/features/app/blocks/view/grid/GridViewBaseInner.tsx b/apps/nextjs-app/src/features/app/blocks/view/grid/GridViewBaseInner.tsx index 6612827072..c7c7262739 100644 --- a/apps/nextjs-app/src/features/app/blocks/view/grid/GridViewBaseInner.tsx +++ b/apps/nextjs-app/src/features/app/blocks/view/grid/GridViewBaseInner.tsx @@ -1,5 +1,4 @@ import { useMutation } from '@tanstack/react-query'; -import type { IAttachmentCellValue, IFieldVo, IGridViewOptions } from '@teable/core'; import { FieldKeyType, FieldType, @@ -8,6 +7,7 @@ import { fieldVoSchema, stringifyClipboardText, } from '@teable/core'; +import type { IAttachmentCellValue, IFieldVo, IGridViewOptions } from '@teable/core'; import type { ICreateRecordsRo, IGroupPointsVo, IUpdateOrderRo } from '@teable/openapi'; import { createRecords, stopFillField, UploadType } from '@teable/openapi'; import type { @@ -97,8 +97,6 @@ import { FieldOperator } from '../../../components/field-setting'; import { useFieldSettingStore } from '../field/useFieldSettingStore'; import { useContextMenu } from '../hooks/useContextMenu'; import { AiGenerateButton, PrefillingRowContainer, PresortRowContainer } from './components'; -import type { IConfirmNewRecordsRef } from './components/ConfirmNewRecords'; -import { ConfirmNewRecords } from './components/ConfirmNewRecords'; import { ResetClickCountButton } from './components/ResetClickCountButton'; import { GIRD_FIELD_NAME_HEIGHT_DEFINITIONS, GIRD_ROW_HEIGHT_DEFINITIONS } from './const'; import { DomBox } from './DomBox'; @@ -113,7 +111,8 @@ interface IGridViewBaseInnerProps { onRowExpand?: (recordId: string) => void; } -const { scrollBuffer, columnAppendBtnWidth } = GRID_DEFAULT; +const { scrollBuffer, columnAppendBtnWidth, columnStatisticHeight } = GRID_DEFAULT; +const MAX_PREFILLING_REGION_HEIGHT = 163; export const GridViewBaseInner: React.FC = ( props: IGridViewBaseInnerProps @@ -153,10 +152,9 @@ export const GridViewBaseInner: React.FC = ( const { openTooltip, closeTooltip } = useGridTooltipStore(); const preTableId = usePrevious(tableId); const isTouchDevice = useIsTouchDevice(); - const sort = view?.sort; - const group = view?.group; + const { sort, group, filter, options } = view ?? {}; const isAutoSort = sort && !sort?.manualSort; - const { frozenFieldId, frozenColumnCount: frozenColumnCountOption } = (view?.options ?? + const { frozenFieldId, frozenColumnCount: frozenColumnCountOption } = (options ?? {}) as IGridViewOptions; const frozenColumnCount = useMemo(() => { return computeFrozenColumnCount({ @@ -168,16 +166,15 @@ export const GridViewBaseInner: React.FC = ( }); }, [isTouchDevice, frozenFieldId, columns, allFields, frozenColumnCountOption]); const { cells: taskStatusCells, fieldMap: taskStatusFieldMap } = taskStatusCollection ?? {}; - const rowHeight = GIRD_ROW_HEIGHT_DEFINITIONS[view?.options?.rowHeight ?? RowHeightLevel.Short]; + const rowHeight = GIRD_ROW_HEIGHT_DEFINITIONS[options?.rowHeight ?? RowHeightLevel.Short]; const columnHeaderHeight = - GIRD_FIELD_NAME_HEIGHT_DEFINITIONS[view?.options?.fieldNameDisplayLines ?? 1]; + GIRD_FIELD_NAME_HEIGHT_DEFINITIONS[options?.fieldNameDisplayLines ?? 1]; const permission = useTablePermission(); const realRowCount = rowCount ?? ssrRecords?.length ?? 0; const fieldEditable = permission['field|update']; const { undo, redo } = useUndoRedo(); const { setGridRef, searchCursor, setRecordMap } = useGridSearchStore(); const [expandRecord, setExpandRecord] = useState<{ tableId: string; recordId: string }>(); - const [newRecords, setNewRecords] = useState(); const [autoFillFieldId, setAutoFillFieldId] = useState(); const { fieldAIEnable = false } = usage?.limit ?? {}; @@ -194,7 +191,6 @@ export const GridViewBaseInner: React.FC = ( const prefillingGridRef = useRef(null); const containerRef = useRef(null); const expandRecordRef = useRef(null); - const confirmNewRecordsRef = useRef(null); const groupCollection = useGridGroupCollection(); @@ -239,20 +235,45 @@ export const GridViewBaseInner: React.FC = ( } = useGridSelection({ recordMap, columns, viewQuery, gridRef }); const { - localRecord, + localRecords, + prefillingRows, prefillingRowIndex, prefillingRowOrder, - prefillingFieldValueMap, + setPrefillingRows, setPrefillingRowIndex, setPrefillingRowOrder, onPrefillingCellEdited, getPrefillingCellContent, - setPrefillingFieldValueMap, } = useGridPrefillingRow(columns); const inPresorting = presortRecord != null; const inPrefilling = prefillingRowIndex != null; + const buildPrefillingInitialFields = useCallback( + async (baseValueMap: { [fieldId: string]: unknown } = {}) => { + const filterValueMap = await extractDefaultFieldsFromFilters({ + filter, + fieldMap: keyBy(allFields, 'id'), + currentUserId: user.id, + }); + let groupValueMap: { [fieldId: string]: unknown } = {}; + if (group?.length && prefillingRowIndex != null) { + const refRecord = recordMap[prefillingRowIndex]; + if (refRecord) { + groupValueMap = group.reduce( + (prev, { fieldId }) => { + prev[fieldId] = refRecord.getCellValue(fieldId); + return prev; + }, + {} as { [fieldId: string]: unknown } + ); + } + } + return { ...baseValueMap, ...groupValueMap, ...filterValueMap }; + }, + [allFields, group, prefillingRowIndex, recordMap, user.id, filter] + ); + const onValidation = useCallback( (cell: ICellItem) => { if (!permission['view|update']) return false; @@ -283,18 +304,21 @@ export const GridViewBaseInner: React.FC = ( const onPrefillingCellDrop = useCallback( async (cell: ICellItem, files: FileList) => { - if (!localRecord) return; - const attachments = await uploadFiles(files, UploadType.Table, baseId); - const [columnIndex] = cell; + const [columnIndex, rowIndex] = cell; const field = fields[columnIndex]; - const oldCellValue = (localRecord.getCellValue(field.id) as IAttachmentCellValue) || []; - setPrefillingFieldValueMap((prev) => ({ - ...prev, - [field.id]: [...oldCellValue, ...attachments], - })); + setPrefillingRows((prev) => { + const next = [...prev]; + const row = next[rowIndex]; + if (!row) return prev; + const oldCellValue = (row.fields[field.id] as IAttachmentCellValue) || []; + next[rowIndex] = { + fields: { ...row.fields, [field.id]: [...oldCellValue, ...attachments] }, + }; + return next; + }); }, - [baseId, fields, localRecord, setPrefillingFieldValueMap] + [baseId, fields, setPrefillingRows] ); useGridFileEvent({ @@ -320,8 +344,7 @@ export const GridViewBaseInner: React.FC = ( const resetNewRecords = () => { setPrefillingRowIndex(undefined); - setPrefillingFieldValueMap(undefined); - setNewRecords(undefined); + setPrefillingRows([]); }; useEffect(() => { @@ -598,43 +621,25 @@ export const GridViewBaseInner: React.FC = ( num?: number ) => { const index = targetIndex ?? Math.max(realRowCount - 1, 0); - if (num === 0) { - return; - } - setPrefillingRowOrder(rowOrder); - const filter = view?.filter; - const fieldMap = keyBy(allFields, 'id'); + if (num === 0) return; - if (num === 1 || num === undefined) { - setPrefillingFieldValueMap(fieldValueMap); + setPrefillingRowOrder(rowOrder); - setPrefillingRowIndex(index); - setSelection(emptySelection); - gridRef.current?.setSelection(emptySelection); - setTimeout(() => { - prefillingGridRef.current?.setSelection( - new CombinedSelection(SelectionRegionType.Cells, [ - [0, 0], - [0, 0], - ]) - ); - }); - } else { - const filterValueMap = await extractDefaultFieldsFromFilters({ - filter, - fieldMap, - currentUserId: user.id, - }); - // insert empty records - const emptyRecords = Array.from({ length: num }).fill({ - fields: { - ...fieldValueMap, - ...filterValueMap, - }, - }) as ICreateRecordsRo['records']; - mutateCreateRecord(emptyRecords); - } + const count = num ?? 1; + const initialFields = await buildPrefillingInitialFields(fieldValueMap); + setPrefillingRows(Array.from({ length: count }).map(() => ({ fields: initialFields }))); + setPrefillingRowIndex(index); + setSelection(emptySelection); + gridRef.current?.setSelection(emptySelection); + setTimeout(() => { + prefillingGridRef.current?.setSelection( + new CombinedSelection(SelectionRegionType.Cells, [ + [0, 0], + [0, 0], + ]) + ); + }); }; const onRowAppend = (targetIndex?: number) => { @@ -696,7 +701,6 @@ export const GridViewBaseInner: React.FC = ( return; } if (isSelectionLoaded({ selection, recordMap, rowCount: realRowCount })) { - // sync copy syncCopy(e, { selection, recordMap }); return; } @@ -730,31 +734,54 @@ export const GridViewBaseInner: React.FC = ( syncCopy(e, { getCopyData }); }; + const onCopyForPrefilling = (selection: CombinedSelection, e: React.ClipboardEvent) => { + if (!localRecords.length) return; + const recordMapForCopy: { [key: number]: Record } = {}; + for (let i = 0; i < localRecords.length; i++) { + recordMapForCopy[i] = localRecords[i]; + } + syncCopy(e, { selection, recordMap: recordMapForCopy }); + }; + const onPaste = async (selection: CombinedSelection, e: React.ClipboardEvent) => { if (!permission['record|update']) { - return toast.warning('Unable to paste'); + return toast.warning(t('table:table.actionTips.pasteError.noPermission')); } await paste(e, selection, recordMap); }; const onPasteForPrefilling = (selection: CombinedSelection, e: React.ClipboardEvent) => { - if (!permission['record|update'] || localRecord == null) { - return toast.warning('Unable to paste'); + if (!localRecords.length) return; + const [start, end] = selection.serialize(); + const startRow = Math.min(start[1], end[1]); + const endRow = Math.max(start[1], end[1]); + const recordMapForPaste: { [key: number]: Record } = {}; + for (let r = startRow; r <= endRow; r++) { + recordMapForPaste[r - startRow] = localRecords[r]; } - paste(e, selection, { 0: localRecord }, (records) => { - if (records.length > 1) { - confirmNewRecordsRef.current?.setOpen(true, records.length); - setNewRecords(records); - return; - } - setPrefillingFieldValueMap({ ...prefillingFieldValueMap, ...records[0].fields }); + paste(e, selection, recordMapForPaste, (records) => { + setPrefillingRows((prev) => { + if (records.length <= 0) return prev; + const baseIndex = startRow; + const next = [...prev]; + for (let i = 0; i < records.length; i++) { + const idx = baseIndex + i; + const rec = records[i]; + if (next[idx]) { + next[idx] = { fields: { ...(next[idx].fields ?? {}), ...(rec.fields ?? {}) } }; + } else { + next[idx] = { fields: { ...(rec.fields ?? {}) } }; + } + } + return next.filter(Boolean); + }); }); }; const onPasteForPresort = (selection: CombinedSelection, e: React.ClipboardEvent) => { if (!presortRecord) return; if (!permission['record|update']) { - return toast.warning('Unable to paste'); + return toast.warning(t('table:table.actionTips.pasteError.noPermission')); } paste(e, selection, { 0: presortRecord }, (records) => { updateRecord({ @@ -771,19 +798,26 @@ export const GridViewBaseInner: React.FC = ( }; const onDeleteForPrefilling = (selection: CombinedSelection) => { - if (localRecord == null || prefillingFieldValueMap == null) return; - const [start, end] = selection.serialize(); const startCol = Math.min(start[0], end[0]); const endCol = Math.max(start[0], end[0]); - - const updated: { [fieldId: string]: unknown } = { ...prefillingFieldValueMap }; - for (let col = startCol; col <= endCol; col++) { - const fieldId = columns[col]?.id; - if (!fieldId) continue; - updated[fieldId] = null; - } - setPrefillingFieldValueMap(updated); + const startRow = Math.min(start[1], end[1]); + const endRow = Math.max(start[1], end[1]); + setPrefillingRows((prev) => { + const next = [...prev]; + for (let row = startRow; row <= endRow; row++) { + const rowData = next[row]; + if (!rowData) continue; + const updated: { [fieldId: string]: unknown } = { ...rowData.fields }; + for (let col = startCol; col <= endCol; col++) { + const fieldId = columns[col]?.id; + if (!fieldId) continue; + updated[fieldId] = null; + } + next[row] = { fields: updated }; + } + return next; + }); }; const onDeleteForPresort = (selection: CombinedSelection) => { @@ -1070,20 +1104,27 @@ export const GridViewBaseInner: React.FC = ( const prefillingRowStyle = useMemo(() => { const defaultTop = rowHeight; - const height = rowHeight + 5; + const height = rowHeight * Math.max(prefillingRows.length, 1) + 3; if (gridRef.current == null || prefillingRowIndex == null) { return { top: 0, height }; } + const minTop = GIRD_ROW_HEIGHT_DEFINITIONS[RowHeightLevel.Short]; + const baseTop = gridRef.current.getRowOffset(prefillingRowIndex) + defaultTop; + const containerHeight = containerRef.current?.clientHeight ?? 0; + const effectiveHeight = Math.min(height, MAX_PREFILLING_REGION_HEIGHT); + const bottomSafe = GIRD_ROW_HEIGHT_DEFINITIONS[RowHeightLevel.Short] + columnStatisticHeight; + const maxTop = + containerHeight > 0 + ? Math.max(minTop, containerHeight - effectiveHeight - bottomSafe) + : Infinity; + return { - top: Math.max( - gridRef.current.getRowOffset(prefillingRowIndex) + defaultTop, - GIRD_ROW_HEIGHT_DEFINITIONS[RowHeightLevel.Short] - ), + top: Math.min(Math.max(baseTop, minTop), maxTop), height, }; - }, [rowHeight, prefillingRowIndex]); + }, [rowHeight, prefillingRowIndex, prefillingRows.length]); const presortRowStyle = useMemo(() => { const height = rowHeight + 5; @@ -1266,15 +1307,56 @@ export const GridViewBaseInner: React.FC = ( )} {inPrefilling && ( { - if (isCreatingRecord || newRecords?.length) return; - await mutateCreateRecord([{ fields: prefillingFieldValueMap! }]); + if (isCreatingRecord || !prefillingRows.length) return; + const requiredFieldIds = allFields + .filter((f) => !f.isComputed && f.notNull) + .map((f) => f.id); + const missingFieldIdSet = new Set(); + if (requiredFieldIds.length) { + for (const row of prefillingRows) { + for (const fid of requiredFieldIds) { + if (isEmptyValue((row.fields ?? {})[fid])) { + missingFieldIdSet.add(fid); + } + } + } + } + if (missingFieldIdSet.size) { + const missingNames = allFields + .filter((f) => missingFieldIdSet.has(f.id)) + .map((f) => f.name); + return toast.warning( + t('table:table.actionTips.requiredFieldsMissing', { + fieldNames: missingNames.join(', '), + }) + ); + } + + await mutateCreateRecord(prefillingRows.map((r) => ({ fields: r.fields }))); }} onCancel={() => { setPrefillingRowIndex(undefined); - setPrefillingFieldValueMap(undefined); + setPrefillingRows([]); + }} + onAddRow={async () => { + const initialFields = await buildPrefillingInitialFields(); + setPrefillingRows((prev) => [...prev, { fields: initialFields }]); + const prefillingRowCount = prefillingRows.length; + prefillingGridRef.current?.scrollToItem([0, prefillingRowCount]); + setTimeout(() => { + prefillingGridRef.current?.setSelection( + new CombinedSelection(SelectionRegionType.Cells, [ + [0, prefillingRowCount], + [0, prefillingRowCount], + ]) + ); + }); }} > = ( scrollBufferX={ permission['field|create'] ? scrollBuffer + columnAppendBtnWidth : scrollBuffer } - scrollBufferY={0} - scrollBarVisible={false} - rowCount={1} + scrollBufferY={1} + scrollBarXVisible={false} + rowCount={prefillingRows.length || 1} rowHeight={rowHeight} - rowIndexVisible={false} rowControls={rowControls} draggable={DraggableType.None} selectable={SelectableType.Cell} @@ -1299,7 +1380,7 @@ export const GridViewBaseInner: React.FC = ( getCellContent={getPrefillingCellContent} onScrollChanged={onPrefillingGridScrollChanged} onCellEdited={onPrefillingCellEdited} - onCopy={(selection, e) => onCopyForSingleRow(e, selection, prefillingFieldValueMap)} + onCopy={onCopyForPrefilling} onPaste={onPasteForPrefilling} onDelete={getAuthorizedFunction(onDeleteForPrefilling, 'record|update')} /> @@ -1318,7 +1399,8 @@ export const GridViewBaseInner: React.FC = ( permission['field|create'] ? scrollBuffer + columnAppendBtnWidth : scrollBuffer } scrollBufferY={0} - scrollBarVisible={false} + scrollBarXVisible={false} + scrollBarYVisible={false} rowCount={1} rowHeight={rowHeight} rowIndexVisible={false} @@ -1358,14 +1440,7 @@ export const GridViewBaseInner: React.FC = ( buttonClickStatusHook={buttonClickStatusHook} /> )} - { - setPrefillingFieldValueMap({ ...prefillingFieldValueMap, ...newRecords?.[0].fields }); - setNewRecords(undefined); - }} - onConfirm={() => newRecords && mutateCreateRecord(newRecords)} - /> + {/* removed legacy ConfirmNewRecords flow */} { diff --git a/apps/nextjs-app/src/features/app/blocks/view/grid/components/PrefillingRowContainer.tsx b/apps/nextjs-app/src/features/app/blocks/view/grid/components/PrefillingRowContainer.tsx index 7cc148f7e6..149828a249 100644 --- a/apps/nextjs-app/src/features/app/blocks/view/grid/components/PrefillingRowContainer.tsx +++ b/apps/nextjs-app/src/features/app/blocks/view/grid/components/PrefillingRowContainer.tsx @@ -18,10 +18,11 @@ interface IPrefillingRowContainerProps { isLoading?: boolean; onCancel?: () => void; onClickOutside?: () => void; + onAddRow?: () => void; } export const PrefillingRowContainer = (props: IPrefillingRowContainerProps) => { - const { style, children, isLoading, onCancel, onClickOutside } = props; + const { style, children, isLoading, onCancel, onClickOutside, onAddRow } = props; const prefillingGridContainerRef = useRef(null); const { t } = useTranslation(tableConfig.i18nNamespaces); @@ -32,10 +33,10 @@ export const PrefillingRowContainer = (props: IPrefillingRowContainerProps) => { return (
-
+
{isLoading ? : } {t('table:grid.prefillingRowTitle')} @@ -58,6 +59,19 @@ export const PrefillingRowContainer = (props: IPrefillingRowContainerProps) => {
{children} +
onAddRow?.()} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + onAddRow?.(); + } + }} + > + +
); }; diff --git a/packages/common-i18n/src/locales/de/table.json b/packages/common-i18n/src/locales/de/table.json index 6569ed7887..9d699cb448 100644 --- a/packages/common-i18n/src/locales/de/table.json +++ b/packages/common-i18n/src/locales/de/table.json @@ -404,7 +404,11 @@ "pasteFileFailed": "Dateien können nur in ein Anhangsfeld eingefügt werden", "copyError": { "noFocus": "Bitte lassen Sie die Seite aktiv und wechseln Sie nicht das Fenster" - } + }, + "pasteError": { + "noPermission": "Sie haben keine Berechtigung, Datensätze einzufügen" + }, + "requiredFieldsMissing": "{{fieldNames}} dürfen nicht leer sein. Bitte füllen Sie die Felder vor dem Absenden aus." }, "graph": { "tableLabel": "Tabelle Label", diff --git a/packages/common-i18n/src/locales/en/table.json b/packages/common-i18n/src/locales/en/table.json index d18b4ed718..a95f1bc771 100644 --- a/packages/common-i18n/src/locales/en/table.json +++ b/packages/common-i18n/src/locales/en/table.json @@ -428,7 +428,11 @@ "copyError": { "noFocus": "Please keep the page active and do not switch windows", "noPermission": "You don't have permission to copy records" - } + }, + "pasteError": { + "noPermission": "You don't have permission to paste records" + }, + "requiredFieldsMissing": "{{fieldNames}} fields cannot be empty. Please complete them before submitting." }, "graph": { "tableLabel": "Table label", diff --git a/packages/common-i18n/src/locales/es/table.json b/packages/common-i18n/src/locales/es/table.json index 6470088793..d01d5c8636 100644 --- a/packages/common-i18n/src/locales/es/table.json +++ b/packages/common-i18n/src/locales/es/table.json @@ -400,7 +400,11 @@ "pasteFileFailed": "Los archivos solo se pueden pegar en un campo de archivo adjunto", "copyError": { "noFocus": "Mantenga la página activa y no cambie Windows" - } + }, + "pasteError": { + "noPermission": "No tienes permiso para pegar registros" + }, + "requiredFieldsMissing": "{{fieldNames}} no pueden estar vacíos. Complete los campos antes de enviar." }, "graph": { "tableLabel": "Etiqueta de mesa", diff --git a/packages/common-i18n/src/locales/fr/table.json b/packages/common-i18n/src/locales/fr/table.json index 871797e440..cf0e411f96 100644 --- a/packages/common-i18n/src/locales/fr/table.json +++ b/packages/common-i18n/src/locales/fr/table.json @@ -395,7 +395,11 @@ "pasteFileFailed": "Les fichiers ne peuvent être collés que dans un champ de pièces jointes", "copyError": { "noFocus": "Veuillez garder la page active et ne pas changer de fenêtre" - } + }, + "pasteError": { + "noPermission": "Vous n'avez pas l'autorisation de coller des enregistrements" + }, + "requiredFieldsMissing": "{{fieldNames}} ne peuvent pas être vides. Veuillez les remplir avant de soumettre." }, "graph": { "tableLabel": "Étiquette de la table", diff --git a/packages/common-i18n/src/locales/it/table.json b/packages/common-i18n/src/locales/it/table.json index eb600912b0..3777f1b02a 100644 --- a/packages/common-i18n/src/locales/it/table.json +++ b/packages/common-i18n/src/locales/it/table.json @@ -404,7 +404,11 @@ "pasteFileFailed": "I file possono essere incollati solo in un campo di allegato", "copyError": { "noFocus": "Si prega di mantenere la pagina attiva e non cambiare finestra" - } + }, + "pasteError": { + "noPermission": "Non hai l'autorizzazione per incollare i record" + }, + "requiredFieldsMissing": "{{fieldNames}} non possono essere vuoti. Compila i campi prima di inviare." }, "graph": { "tableLabel": "Etichetta della tabella", diff --git a/packages/common-i18n/src/locales/ja/table.json b/packages/common-i18n/src/locales/ja/table.json index abd24e06d3..9c6fb63d8b 100644 --- a/packages/common-i18n/src/locales/ja/table.json +++ b/packages/common-i18n/src/locales/ja/table.json @@ -395,7 +395,11 @@ "pasteFileFailed": "ファイルは添付ファイル欄にのみ貼り付けることができます", "copyError": { "noFocus": "ページをアクティブに保ち、ウィンドウを切り替えないでください" - } + }, + "pasteError": { + "noPermission": "レコードを貼り付ける権限がありません" + }, + "requiredFieldsMissing": "{{fieldNames}} は空にできません。送信する前に入力してください。" }, "graph": { "tableLabel": "テーブルラベル", diff --git a/packages/common-i18n/src/locales/ru/table.json b/packages/common-i18n/src/locales/ru/table.json index 80929e7d62..bfe6440c6f 100644 --- a/packages/common-i18n/src/locales/ru/table.json +++ b/packages/common-i18n/src/locales/ru/table.json @@ -395,7 +395,11 @@ "pasteFileFailed": "Файлы можно вставлять только в поле вложений", "copyError": { "noFocus": "Пожалуйста, не меняйте окно" - } + }, + "pasteError": { + "noPermission": "У вас нет прав вставлять записи" + }, + "requiredFieldsMissing": "Поля {{fieldNames}} не могут быть пустыми. Заполните их перед отправкой." }, "graph": { "tableLabel": "Метка таблицы", diff --git a/packages/common-i18n/src/locales/tr/table.json b/packages/common-i18n/src/locales/tr/table.json index 29a73725f5..b93910c378 100644 --- a/packages/common-i18n/src/locales/tr/table.json +++ b/packages/common-i18n/src/locales/tr/table.json @@ -393,7 +393,11 @@ "pasteFileFailed": "Dosyalar yalnızca bir ek alanına yapıştırılabilir", "copyError": { "noFocus": "Lütfen pencereyi değiştirmeyin" - } + }, + "pasteError": { + "noPermission": "Kayıtları yapıştırma izniniz yok" + }, + "requiredFieldsMissing": "{{fieldNames}} boş bırakılamaz. Lütfen göndermeden önce doldurun." }, "graph": { "tableLabel": "Tablo etiketi", diff --git a/packages/common-i18n/src/locales/uk/table.json b/packages/common-i18n/src/locales/uk/table.json index dcc13793f8..106c9f92c0 100644 --- a/packages/common-i18n/src/locales/uk/table.json +++ b/packages/common-i18n/src/locales/uk/table.json @@ -404,7 +404,11 @@ "pasteFileFailed": "Файли можна вставляти лише в поле вкладення", "copyError": { "noFocus": "Будь ласка, тримайте сторінку активною та не перемикайте вікна" - } + }, + "pasteError": { + "noPermission": "У вас немає дозволу вставляти записи" + }, + "requiredFieldsMissing": "Поля {{fieldNames}} не можуть бути порожніми. Будь ласка, заповніть їх перед надсиланням." }, "graph": { "tableLabel": "Мітка таблиці", diff --git a/packages/common-i18n/src/locales/zh/table.json b/packages/common-i18n/src/locales/zh/table.json index 367a960f14..0cab51d2db 100644 --- a/packages/common-i18n/src/locales/zh/table.json +++ b/packages/common-i18n/src/locales/zh/table.json @@ -429,7 +429,11 @@ "copyError": { "noFocus": "请保持页面激活,不要切换窗口", "noPermission": "您没有权限复制记录" - } + }, + "pasteError": { + "noPermission": "您没有权限粘贴记录" + }, + "requiredFieldsMissing": "{{fieldNames}} 字段不能为空,请填写完整后再提交" }, "graph": { "tableLabel": "表格名称", diff --git a/packages/sdk/src/components/grid-enhancements/hooks/use-grid-popup-position.tsx b/packages/sdk/src/components/grid-enhancements/hooks/use-grid-popup-position.tsx index ce837f0981..4c8ac6c33e 100644 --- a/packages/sdk/src/components/grid-enhancements/hooks/use-grid-popup-position.tsx +++ b/packages/sdk/src/components/grid-enhancements/hooks/use-grid-popup-position.tsx @@ -24,7 +24,7 @@ export const useGridPopupPosition = (rect: IEditorProps['rect'], maxHeight?: num return { top: isAbove ? 'unset' : height + 1, bottom: isAbove ? height : 'unset', - maxHeight: finalHeight, + maxHeight: maxHeight ?? finalHeight, }; }, [editorId, y, height, maxHeight]); }; diff --git a/packages/sdk/src/components/grid-enhancements/hooks/use-grid-prefilling-row.ts b/packages/sdk/src/components/grid-enhancements/hooks/use-grid-prefilling-row.ts index 85d890b74e..13a494860a 100644 --- a/packages/sdk/src/components/grid-enhancements/hooks/use-grid-prefilling-row.ts +++ b/packages/sdk/src/components/grid-enhancements/hooks/use-grid-prefilling-row.ts @@ -20,51 +20,47 @@ export const useGridPrefillingRow = (columns: (IGridColumn & { id: string })[]) const [prefillingRowOrder, setPrefillingRowOrder] = useState(); const [prefillingRowIndex, setPrefillingRowIndex] = useState(); - const [prefillingFieldValueMap, setPrefillingFieldValueMap] = useState< - { [fieldId: string]: unknown } | undefined - >(); + const [prefillingRows, setPrefillingRows] = useState< + { fields: { [fieldId: string]: unknown } }[] + >([]); - const localRecord = useMemo(() => { - if (prefillingFieldValueMap == null) { - return null; - } - - const record = createRecordInstance({ - id: '', - fields: prefillingFieldValueMap, - }); - record.getCellValue = (fieldId: string) => { - return prefillingFieldValueMap[fieldId]; - }; - record.updateCell = (fieldId: string, newValue: unknown) => { - record.fields[fieldId] = newValue; - setPrefillingFieldValueMap({ - ...prefillingFieldValueMap, - [fieldId]: newValue, + const localRecords = useMemo(() => { + return prefillingRows.map((row, index) => { + const record = createRecordInstance({ + id: '', + fields: row.fields as never, }); - return Promise.resolve(); - }; + record.getCellValue = (fieldId: string) => { + return prefillingRows[index]?.fields?.[fieldId]; + }; + record.updateCell = (fieldId: string, newValue: unknown) => { + setPrefillingRows((prev) => { + const next = [...prev]; + const current = next[index] ?? { fields: {} }; + next[index] = { fields: { ...current.fields, [fieldId]: newValue } }; + return next; + }); + return Promise.resolve(); + }; + return record; + }); + }, [prefillingRows, setPrefillingRows]); - return record; - }, [prefillingFieldValueMap]); const createCellValue2GridDisplay = useCreateCellValue2GridDisplay(); const getPrefillingCellContent = useCallback<(cell: ICellItem) => ICell>( (cell) => { - const [columnIndex] = cell; + const [columnIndex, rowIndex] = cell; const cellValue2GridDisplay = createCellValue2GridDisplay(fields); - if (localRecord != null) { - const fieldId = columns[columnIndex]?.id; - if (!fieldId) return { type: CellType.Loading }; - return cellValue2GridDisplay(localRecord, columnIndex, true); - } - return { type: CellType.Loading }; + const record = localRecords[rowIndex ?? 0]; + if (!record) return { type: CellType.Loading }; + if (columns[columnIndex]?.id == null) return { type: CellType.Loading }; + return cellValue2GridDisplay(record, columnIndex, true); }, - [columns, createCellValue2GridDisplay, fields, localRecord] + [columns, createCellValue2GridDisplay, fields, localRecords] ); useEffect(() => { if (prefillingRowIndex == null) return; - const updateDefaultValue = async () => { const fieldValue = await extractDefaultFieldsFromFilters({ filter, @@ -74,12 +70,11 @@ export const useGridPrefillingRow = (columns: (IGridColumn & { id: string })[]) tableId, isAsync: true, }); - setPrefillingFieldValueMap((prev) => { - if (prev == null) return; - return { - ...prev, - ...fieldValue, - }; + setPrefillingRows((prev) => { + const next = prev.length ? [...prev] : [{ fields: {} }]; + const first = next[0] ?? { fields: {} }; + next[0] = { fields: { ...first.fields, ...fieldValue } }; + return next; }); }; updateDefaultValue(); @@ -88,13 +83,10 @@ export const useGridPrefillingRow = (columns: (IGridColumn & { id: string })[]) const onPrefillingCellEdited = useCallback( (cell: ICellItem, newVal: IInnerCell) => { - if (localRecord == null) return; - - const [col] = cell; + const [col, rowIndex = 0] = cell; const fieldId = columns[col].id; const { type, data } = newVal; let newCellValue: unknown = null; - switch (type) { case CellType.Select: newCellValue = data?.length ? data : null; @@ -105,35 +97,32 @@ export const useGridPrefillingRow = (columns: (IGridColumn & { id: string })[]) default: newCellValue = data === '' ? null : data; } - const oldCellValue = localRecord.getCellValue(fieldId) ?? null; + const record = localRecords[rowIndex]; + const oldCellValue = record?.getCellValue(fieldId) ?? null; if (isEqual(newCellValue, oldCellValue)) return; - localRecord.updateCell(fieldId, newCellValue); - return localRecord; + if (record?.updateCell) { + record.updateCell(fieldId, newCellValue); + } else { + setPrefillingRows((prev) => { + const next = prev.length ? [...prev] : [{ fields: {} }]; + const row = next[rowIndex] ?? { fields: {} }; + next[rowIndex] = { fields: { ...row.fields, [fieldId]: newCellValue } }; + return next; + }); + } }, - [localRecord, columns] + [columns, localRecords, setPrefillingRows] ); - return useMemo(() => { - return { - localRecord, - prefillingRowIndex, - prefillingRowOrder, - prefillingFieldValueMap, - setPrefillingRowIndex, - setPrefillingRowOrder, - onPrefillingCellEdited, - getPrefillingCellContent, - setPrefillingFieldValueMap, - }; - }, [ - localRecord, + return { + prefillingRows, prefillingRowIndex, prefillingRowOrder, - prefillingFieldValueMap, + localRecords, + setPrefillingRows, setPrefillingRowIndex, setPrefillingRowOrder, onPrefillingCellEdited, getPrefillingCellContent, - setPrefillingFieldValueMap, - ]); + }; }; diff --git a/packages/sdk/src/components/grid/Grid.tsx b/packages/sdk/src/components/grid/Grid.tsx index ac687b79ba..522ce0ddc0 100644 --- a/packages/sdk/src/components/grid/Grid.tsx +++ b/packages/sdk/src/components/grid/Grid.tsx @@ -49,7 +49,8 @@ export interface IGridExternalProps { smoothScrollY?: boolean; scrollBufferX?: number; scrollBufferY?: number; - scrollBarVisible?: boolean; + scrollBarXVisible?: boolean; + scrollBarYVisible?: boolean; rowIndexVisible?: boolean; collaborators?: ICollaborator; // [rowIndex, colIndex] @@ -194,7 +195,8 @@ const GridBase: ForwardRefRenderFunction = (props, forward smoothScrollY = true, scrollBufferX = scrollBuffer, scrollBufferY = scrollBuffer, - scrollBarVisible = true, + scrollBarXVisible = true, + scrollBarYVisible = true, rowIndexVisible = true, isMultiSelectionEnable = true, style, @@ -586,7 +588,7 @@ const GridBase: ForwardRefRenderFunction = (props, forward const rowHeight = coordInstance.getRowHeight(rowIndex); const offsetY = coordInstance.getRowOffset(rowIndex); const deltaTop = Math.min(offsetY - scrollTop - rowInitSize, 0); - const deltaBottom = Math.max(offsetY + rowHeight - scrollTop - containerHeight, 0); + const deltaBottom = Math.max(offsetY + rowHeight - scrollTop - containerHeight + 1, 0); const st = scrollTop + deltaTop + deltaBottom; if (st !== scrollTop) { scrollTo(undefined, st); @@ -727,7 +729,8 @@ const GridBase: ForwardRefRenderFunction = (props, forward scrollHeight={totalHeight} smoothScrollX={smoothScrollX} smoothScrollY={smoothScrollY} - scrollBarVisible={scrollBarVisible} + scrollBarXVisible={scrollBarXVisible} + scrollBarYVisible={scrollBarYVisible} containerRef={containerRef} scrollState={scrollState} scrollEnable={scrollEnable} diff --git a/packages/sdk/src/components/grid/InfiniteScroller.tsx b/packages/sdk/src/components/grid/InfiniteScroller.tsx index 9d645b99b5..a09fd9b819 100644 --- a/packages/sdk/src/components/grid/InfiniteScroller.tsx +++ b/packages/sdk/src/components/grid/InfiniteScroller.tsx @@ -18,7 +18,8 @@ export interface ScrollerProps IGridProps, | 'smoothScrollX' | 'smoothScrollY' - | 'scrollBarVisible' + | 'scrollBarXVisible' + | 'scrollBarYVisible' | 'onScrollChanged' | 'onVisibleRegionChanged' > { @@ -53,7 +54,8 @@ const InfiniteScrollerBase: ForwardRefRenderFunction containerRef, smoothScrollX, smoothScrollY, - scrollBarVisible, + scrollBarXVisible, + scrollBarYVisible, scrollEnable = true, scrollState, getLinearRow, @@ -282,7 +284,7 @@ const InfiniteScrollerBase: ForwardRefRenderFunction ref={horizontalScrollRef} className={cn( 'scrollbar scrollbar-thumb-foreground/40 scrollbar-thumb-rounded-md scrollbar-h-[10px] absolute bottom-[2px] left-0 h-4 cursor-pointer overflow-y-hidden overflow-x-scroll will-change-transform', - !scrollBarVisible && 'opacity-0 pointer-events-none' + !scrollBarXVisible && 'opacity-0 pointer-events-none' )} style={{ left, @@ -302,7 +304,7 @@ const InfiniteScrollerBase: ForwardRefRenderFunction ref={verticalScrollRef} className={cn( 'scrollbar scrollbar-thumb-foreground/40 scrollbar-thumb-rounded-md scrollbar-w-[10px] scrollbar-min-thumb absolute right-[2px] w-4 cursor-pointer overflow-x-hidden overflow-y-scroll will-change-transform', - !scrollBarVisible && 'opacity-0 pointer-events-none' + !scrollBarYVisible && 'opacity-0 pointer-events-none' )} style={{ top,