Skip to content

Commit

Permalink
perf: attachment cells support drag and drop uploading (#1255)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sky-FE authored Jan 17, 2025
1 parent 4b338ba commit f8064c8
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -77,6 +80,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';
Expand All @@ -102,6 +106,7 @@ export const GridViewBaseInner: React.FC<IGridViewBaseInnerProps> = (
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;
Expand Down Expand Up @@ -165,13 +170,37 @@ export const GridViewBaseInner: React.FC<IGridViewBaseInnerProps> = (

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,
Expand Down Expand Up @@ -787,7 +816,13 @@ export const GridViewBaseInner: React.FC<IGridViewBaseInnerProps> = (
}, [setGridRef]);

return (
<div ref={containerRef} className="relative size-full">
<div
ref={containerRef}
className="relative size-full"
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
>
<Grid
ref={gridRef}
theme={theme}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './use-grid-group-collection';
export * from './use-grid-collapsed-group';
export * from './use-grid-prefilling-row';
export * from './use-grid-selection';
export * from './use-grid-file-event';
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { DragEvent } from 'react';
import { useRef } from 'react';
import type { IGridRef } from '../../grid/Grid';
import { SelectionRegionType, type ICellItem } from '../../grid/interface';
import { CombinedSelection, emptySelection } from '../../grid/managers';

interface IUseGridFileEventProps {
gridRef: React.RefObject<IGridRef>;
onValidation: (cell: ICellItem) => boolean;
onCellDrop: (cell: ICellItem, files: FileList) => Promise<void> | void;
}

export const useGridFileEvent = (props: IUseGridFileEventProps) => {
const { gridRef, onValidation, onCellDrop } = props;
const dropTargetRef = useRef<ICellItem | null>(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,
};
};
12 changes: 12 additions & 0 deletions packages/sdk/src/components/grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -230,6 +231,17 @@ const GridBase: ForwardRefRenderFunction<IGridRef, IGridProps> = (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;
Expand Down
6 changes: 2 additions & 4 deletions packages/sdk/src/components/grid/InteractionLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,6 @@ export const InteractionLayerBase: ForwardRefRenderFunction<

const { onAutoScroll, onAutoScrollStop } = useAutoScroll({
coordInstance,
isSelecting,
isDragging,
dragType,
scrollBy,
});

Expand Down Expand Up @@ -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);
Expand Down
16 changes: 6 additions & 10 deletions packages/sdk/src/components/grid/hooks/useAutoScroll.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -9,37 +9,33 @@ 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<
[xDir: IScrollDirection, yDir: IScrollDirection]
>([0, 0]);
const [xDirection, yDirection] = scrollDirection || [0, 0];

const onAutoScroll = (mouseState: IMouseState) => {
if (!isSelecting && !isDragging) return;
const { x, y } = mouseState;
const onAutoScroll = <T extends IPosition>(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)) {
xDir = -1;
}
}

if (isSelecting || (isDragging && dragType === DragRegionType.Rows)) {
if (!dragType || dragType === DragRegionType.Rows) {
if (containerHeight - y < threshold) {
yDir = 1;
} else if (inRange(y, rowInitSize, rowInitSize + threshold)) {
Expand Down

0 comments on commit f8064c8

Please sign in to comment.