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 8be28b744..0eb643835 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,14 +1,15 @@ import { useMutation } from '@tanstack/react-query'; -import type { IFieldVo } from '@teable/core'; +import type { IAttachmentCellValue, IFieldVo } from '@teable/core'; import { FieldKeyType, + FieldType, RowHeightLevel, contractColorForTheme, fieldVoSchema, stringifyClipboardText, } from '@teable/core'; import type { ICreateRecordsRo, IGroupPointsVo, IUpdateOrderRo } from '@teable/openapi'; -import { createRecords } from '@teable/openapi'; +import { createRecords, UploadType } from '@teable/openapi'; import type { IRectangle, IPosition, @@ -51,10 +52,12 @@ import { useGridSelection, Record, DragRegionType, + useGridFileEvent, } from '@teable/sdk'; import { GRID_DEFAULT } from '@teable/sdk/components/grid/configs'; import { useScrollFrameRate } from '@teable/sdk/components/grid/hooks'; import { + useBaseId, useFieldCellEditable, useFields, useIsTouchDevice, @@ -76,6 +79,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { usePrevious, useClickAway } from 'react-use'; import { ExpandRecordContainer } from '@/features/app/components/ExpandRecordContainer'; import type { IExpandRecordContainerRef } from '@/features/app/components/ExpandRecordContainer/types'; +import { uploadFiles } from '@/features/app/utils/uploadFile'; import { tableConfig } from '@/features/i18n/table.config'; import { FieldOperator } from '../../../components/field-setting'; import { useFieldSettingStore } from '../field/useFieldSettingStore'; @@ -101,6 +105,7 @@ export const GridViewBaseInner: React.FC = ( const { groupPointsServerData, onRowExpand } = props; const { t } = useTranslation(tableConfig.i18nNamespaces); const router = useRouter(); + const baseId = useBaseId(); const tableId = useTableId() as string; const activeViewId = useViewId(); const view = useView(activeViewId) as GridView | undefined; @@ -162,13 +167,37 @@ export const GridViewBaseInner: React.FC = ( const { presortRecord, - onSelectionChanged, presortRecordData, + onSelectionChanged, onPresortCellEdited, getPresortCellContent, setPresortRecordData, } = useGridSelection({ recordMap, columns, viewQuery, gridRef }); + const { onDragOver, onDragLeave, onDrop } = useGridFileEvent({ + gridRef, + onValidation: (cell) => { + if (!permission['view|update']) return false; + + const [columnIndex] = cell; + const field = fields[columnIndex]; + + if (!field) return false; + + const { type, isComputed } = field; + return type === FieldType.Attachment && !isComputed; + }, + onCellDrop: async (cell, files) => { + const attachments = await uploadFiles(files, UploadType.Table, baseId); + + const [fieldIndex, recordIndex] = cell; + const record = recordMap[recordIndex]; + const field = fields[fieldIndex]; + const oldCellValue = (record.getCellValue(field.id) as IAttachmentCellValue) || []; + await record.updateCell(field.id, [...oldCellValue, ...attachments]); + }, + }); + const { localRecord, prefillingRowIndex, @@ -784,7 +813,13 @@ export const GridViewBaseInner: React.FC = ( }, [setGridRef]); return ( -
+
; + onValidation: (cell: ICellItem) => boolean; + onCellDrop: (cell: ICellItem, files: FileList) => Promise | void; +} + +export const useGridFileEvent = (props: IUseGridFileEventProps) => { + const { gridRef, onValidation, onCellDrop } = props; + const dropTargetRef = useRef(null); + + const getDropCell = (event: DragEvent): ICellItem | null => { + const rect = (event.currentTarget as HTMLElement).getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + return gridRef.current?.getCellIndicesAtPosition(x, y) ?? null; + }; + + const onDragLeave = (event: DragEvent) => { + event.preventDefault(); + gridRef.current?.setSelection(emptySelection); + }; + + const onDragOver = (event: DragEvent) => { + event.preventDefault(); + if (!onCellDrop) return; + + const cell = getDropCell(event); + + if (!cell || !onValidation(cell)) return; + + dropTargetRef.current = cell; + + const newSelection = new CombinedSelection(SelectionRegionType.Cells, [cell, cell]); + gridRef.current?.setSelection(newSelection); + }; + + const onDrop = (event: DragEvent) => { + event.preventDefault(); + gridRef.current?.setSelection(emptySelection); + + if (!onCellDrop || !dropTargetRef.current) return; + + const files = event.dataTransfer?.files; + + if (!files?.length) return; + + onCellDrop(dropTargetRef.current, files); + dropTargetRef.current = null; + }; + + return { + onDragOver, + onDragLeave, + onDrop, + }; +}; diff --git a/packages/sdk/src/components/grid/Grid.tsx b/packages/sdk/src/components/grid/Grid.tsx index 0f464b22b..8b63869fa 100644 --- a/packages/sdk/src/components/grid/Grid.tsx +++ b/packages/sdk/src/components/grid/Grid.tsx @@ -143,6 +143,7 @@ export interface IGridRef { scrollBy: (deltaX: number, deltaY: number) => void; scrollTo: (scrollLeft?: number, scrollTop?: number) => void; scrollToItem: (position: [columnIndex: number, rowIndex: number]) => void; + getCellIndicesAtPosition: (x: number, y: number) => ICellItem | null; } const { @@ -230,6 +231,17 @@ const GridBase: ForwardRefRenderFunction = (props, forward scrollTo, scrollToItem, getScrollState: () => scrollState, + getCellIndicesAtPosition: (x: number, y: number): ICellItem | null => { + const { scrollLeft, scrollTop } = scrollState; + + const rowIndex = coordInstance.getRowStartIndex(scrollTop + y); + const columnIndex = coordInstance.getColumnStartIndex(scrollLeft + x); + + const { type, realIndex } = getLinearRow(rowIndex); + if (type !== LinearRowType.Row) return null; + + return [columnIndex, realIndex]; + }, })); const hasAppendRow = onRowAppend != null; diff --git a/packages/sdk/src/components/grid/InteractionLayer.tsx b/packages/sdk/src/components/grid/InteractionLayer.tsx index 19fda4f3e..1be455d80 100644 --- a/packages/sdk/src/components/grid/InteractionLayer.tsx +++ b/packages/sdk/src/components/grid/InteractionLayer.tsx @@ -292,9 +292,6 @@ export const InteractionLayerBase: ForwardRefRenderFunction< const { onAutoScroll, onAutoScrollStop } = useAutoScroll({ coordInstance, - isSelecting, - isDragging, - dragType, scrollBy, }); @@ -606,7 +603,8 @@ export const InteractionLayerBase: ForwardRefRenderFunction< setMouseState(() => mouseState); setCursorStyle(mouseState.type); onCellPosition(mouseState); - onAutoScroll(mouseState); + if (isSelecting) onAutoScroll(mouseState); + if (isDragging) onAutoScroll(mouseState, dragType); onSelectionChange(mouseState); onColumnResizeChange(mouseState, (newWidth, columnIndex) => { onColumnResize?.(columns[columnIndex], newWidth, columnIndex); diff --git a/packages/sdk/src/components/grid/hooks/useAutoScroll.ts b/packages/sdk/src/components/grid/hooks/useAutoScroll.ts index ee9cb7ec0..eb702885a 100644 --- a/packages/sdk/src/components/grid/hooks/useAutoScroll.ts +++ b/packages/sdk/src/components/grid/hooks/useAutoScroll.ts @@ -1,6 +1,6 @@ import { inRange } from 'lodash'; import { useState, useRef, useEffect } from 'react'; -import type { IMouseState, IScrollDirection } from '../interface'; +import type { IPosition, IScrollDirection } from '../interface'; import { DragRegionType } from '../interface'; import type { CoordinateManager } from '../managers'; @@ -9,15 +9,12 @@ const maxPxPerMs = 2; const msToFullSpeed = 1200; interface IUseAutoScroll { - isSelecting: boolean; - dragType: DragRegionType; - isDragging: boolean; coordInstance: CoordinateManager; scrollBy: (deltaX: number, deltaY: number) => void; } export const useAutoScroll = (props: IUseAutoScroll) => { - const { coordInstance, isSelecting, dragType, isDragging, scrollBy } = props; + const { coordInstance, scrollBy } = props; const speedScalar = useRef(0); const { containerWidth, containerHeight, freezeRegionWidth, rowInitSize } = coordInstance; const [scrollDirection, setScrollDirection] = useState< @@ -25,13 +22,12 @@ export const useAutoScroll = (props: IUseAutoScroll) => { >([0, 0]); const [xDirection, yDirection] = scrollDirection || [0, 0]; - const onAutoScroll = (mouseState: IMouseState) => { - if (!isSelecting && !isDragging) return; - const { x, y } = mouseState; + const onAutoScroll = (position: T, dragType?: DragRegionType) => { + const { x, y } = position; let xDir: IScrollDirection = 0; let yDir: IScrollDirection = 0; - if (isSelecting || (isDragging && dragType === DragRegionType.Columns)) { + if (!dragType || dragType === DragRegionType.Columns) { if (containerWidth - x < threshold) { xDir = 1; } else if (inRange(x, freezeRegionWidth, freezeRegionWidth + threshold)) { @@ -39,7 +35,7 @@ export const useAutoScroll = (props: IUseAutoScroll) => { } } - if (isSelecting || (isDragging && dragType === DragRegionType.Rows)) { + if (!dragType || dragType === DragRegionType.Rows) { if (containerHeight - y < threshold) { yDir = 1; } else if (inRange(y, rowInitSize, rowInitSize + threshold)) {