Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: attachment cells support drag and drop uploading #1255

Merged
merged 1 commit into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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';
Expand All @@ -101,6 +105,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 @@ -162,13 +167,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 @@ -784,7 +813,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
Loading