From f70aef68be4887e8a537f8c7e93e03e052101952 Mon Sep 17 00:00:00 2001 From: fangwufeng-v <141254101+fangwufeng-v@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:13:25 +0800 Subject: [PATCH] feat: drag and drop a row in the Grid (#3376) * feat: new style ui * feat: drag row * feat: hooks for auto scroll feature when dragging * feat: shared UI status that control auto scroll when dragging row. * feat: optimize the drag interaction UI effect * feat: refactor drag positon * feat: drag column header * feat: fix review issue --- frontend/appflowy_tauri/.eslintrc.cjs | 2 +- .../appflowy_tauri/src-tauri/tauri.conf.json | 1 + .../src/appflowy_app/assets/drag.svg | 8 + .../components/database/Database.tsx | 41 ++++ .../components/database/DatabaseHeader.tsx | 47 +++++ .../components/database/_shared/CellText.tsx | 21 ++ .../database/_shared/VirtualizedList.tsx | 46 +++++ .../components/database/_shared/constants.ts | 9 + .../database/_shared/dnd/dnd.context.ts | 17 ++ .../database/_shared/dnd/drag.hooks.ts | 107 +++++++++++ .../database/_shared/dnd/drop.hooks.ts | 88 +++++++++ .../components/database/_shared/dnd/index.ts | 7 + .../components/database/_shared/dnd/utils.ts | 180 ++++++++++++++++++ .../components/database/_shared/index.ts | 6 + .../components/database/database.context.ts | 4 +- .../components/database/database_bd_svc.ts | 24 ++- .../database/grid/GridCell/GridCell.tsx | 4 +- .../grid/GridCell/GridCheckboxCell.tsx | 2 +- .../GridSelectCell/GridSelectCell.tsx | 4 +- .../database/grid/GridCell/GridTextCell.tsx | 11 +- .../database/grid/GridField/GridField.tsx | 143 +++++++++++--- .../grid/GridRow/GridCalculateRow.tsx | 3 + .../database/grid/GridRow/GridCellRow.tsx | 156 ++++++++++++--- .../grid/GridRow/GridCellRowActions.tsx | 36 ++++ .../database/grid/GridRow/GridFieldRow.tsx | 58 ++---- .../database/grid/GridRow/GridNewRow.tsx | 21 +- .../database/grid/GridRow/GridRow.tsx | 32 ++-- .../database/grid/GridRow/constants.ts | 7 +- .../database/grid/GridTable/GridTable.tsx | 74 +++---- .../grid/GridTable/VirtualizedRows.tsx | 78 -------- .../appflowy_app/components/database/index.ts | 2 +- .../src/appflowy_app/utils/tool.ts | 66 ++++++- .../src/appflowy_app/views/DatabasePage.tsx | 37 +--- frontend/resources/translations/en.json | 7 +- 34 files changed, 1062 insertions(+), 287 deletions(-) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/assets/drag.svg create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseHeader.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/CellText.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/VirtualizedList.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/constants.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/dnd.context.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drag.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drop.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/utils.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCalculateRow.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRowActions.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/VirtualizedRows.tsx diff --git a/frontend/appflowy_tauri/.eslintrc.cjs b/frontend/appflowy_tauri/.eslintrc.cjs index 6bb08b783d..7223e5cb39 100644 --- a/frontend/appflowy_tauri/.eslintrc.cjs +++ b/frontend/appflowy_tauri/.eslintrc.cjs @@ -23,6 +23,7 @@ module.exports = { '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/no-namespace': 'error', '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-redeclare': 'error', '@typescript-eslint/prefer-for-of': 'warn', '@typescript-eslint/triple-slash-reference': 'error', '@typescript-eslint/unified-signatures': 'warn', @@ -42,7 +43,6 @@ module.exports = { 'no-invalid-this': 'error', 'no-new-wrappers': 'error', 'no-param-reassign': 'error', - 'no-redeclare': 'error', 'no-sequences': 'error', 'no-throw-literal': 'error', 'no-unsafe-finally': 'error', diff --git a/frontend/appflowy_tauri/src-tauri/tauri.conf.json b/frontend/appflowy_tauri/src-tauri/tauri.conf.json index 6b528bf4fb..9908928657 100644 --- a/frontend/appflowy_tauri/src-tauri/tauri.conf.json +++ b/frontend/appflowy_tauri/src-tauri/tauri.conf.json @@ -73,6 +73,7 @@ }, "windows": [ { + "fileDropEnabled": false, "fullscreen": false, "height": 1200, "resizable": true, diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/drag.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/drag.svg new file mode 100644 index 0000000000..627c959f9f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/drag.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx new file mode 100644 index 0000000000..a770d8ad2d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx @@ -0,0 +1,41 @@ +import { useEffect, useRef, useState } from 'react'; +import { proxy } from 'valtio'; +import { subscribeKey } from 'valtio/utils'; +import { DatabaseLayoutPB } from '@/services/backend'; +import { DndContext, DndContextDescriptor } from './_shared'; +import { VerticalScrollElementRefContext, DatabaseContext } from './database.context'; +import { useViewId, useConnectDatabase } from './database.hooks'; +import { DatabaseHeader } from './DatabaseHeader'; +import { Grid } from './grid'; + +export const Database = () => { + const viewId = useViewId(); + const verticalScrollElementRef = useRef(null); + const database = useConnectDatabase(viewId); + const [ layoutType, setLayoutType ] = useState(database.layoutType); + const dndContext = useRef(proxy({ + dragging: null, + })); + + useEffect(() => { + return subscribeKey(database, 'layoutType', (value) => { + setLayoutType(value); + }); + }, [database]); + + return ( +
+ + + + + {layoutType === DatabaseLayoutPB.Grid ? : null} + + + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseHeader.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseHeader.tsx new file mode 100644 index 0000000000..2c1a5ca0e3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseHeader.tsx @@ -0,0 +1,47 @@ +import { FormEventHandler, useCallback, useEffect, useMemo, useState } from 'react'; +import { t } from 'i18next'; +import { PageController } from '$app/stores/effects/workspace/page/page_controller'; +import { useViewId } from './database.hooks'; + +export const DatabaseHeader = () => { + const viewId = useViewId(); + const [ title, setTitle ] = useState(''); + + const controller = useMemo(() => new PageController(viewId), [ viewId ]); + + useEffect(() => { + void controller.getPage().then(page => { + setTitle(page.name); + }); + + void controller.subscribe({ + onPageChanged: (page) => { + setTitle(page.name); + }, + }); + + return () => { + void controller.unsubscribe(); + }; + }, [ controller ]); + + const handleInput = useCallback((event) => { + const newTitle = (event.target as HTMLInputElement).value; + + void controller.updatePage({ + id: viewId, + name: newTitle, + }); + }, [ viewId, controller ]); + + return ( +
+ +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/CellText.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/CellText.tsx new file mode 100644 index 0000000000..1607b153c5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/CellText.tsx @@ -0,0 +1,21 @@ +import React, { HTMLAttributes, PropsWithChildren } from 'react'; + +export interface CellTextProps { + className?: string; +} + +export const CellText = React.forwardRef>>(function CellText(props, ref) { + const { children, className, ...other } = props; + + return ( +
+ + {children} + +
+ ); +}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/VirtualizedList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/VirtualizedList.tsx new file mode 100644 index 0000000000..61982ea609 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/VirtualizedList.tsx @@ -0,0 +1,46 @@ +import { Virtualizer } from '@tanstack/react-virtual'; +import React, { CSSProperties, FC } from 'react'; + +export interface VirtualizedListProps { + className?: string; + style?: CSSProperties | undefined; + virtualizer: Virtualizer, + itemClassName?: string; + renderItem: (index: number) => React.ReactNode; +} + +export const VirtualizedList: FC = ({ + className, + style, + itemClassName, + virtualizer, + renderItem, +}) => { + const virtualItems = virtualizer.getVirtualItems(); + const { horizontal } = virtualizer.options; + const sizeProp = horizontal ? 'width' : 'height'; + const before = virtualItems.at(0)?.start ?? 0; + const after = virtualizer.getTotalSize() - (virtualItems.at(-1)?.end ?? 0); + + return ( +
+ {before > 0 &&
} + {virtualItems.map((virtualItem) => { + const { key, index, size } = virtualItem; + + return ( +
+ {renderItem(index)} +
+ ); + })} + {after > 0 &&
} +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/constants.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/constants.ts new file mode 100644 index 0000000000..fd1aab7a37 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/constants.ts @@ -0,0 +1,9 @@ +export enum DragType { + Row = 'row', + Field = 'field', +} + +export enum DropPosition { + Before = 0, + After = 1, +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/dnd.context.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/dnd.context.ts new file mode 100644 index 0000000000..8954dc733a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/dnd.context.ts @@ -0,0 +1,17 @@ +import { createContext } from 'react'; +import { proxy } from 'valtio'; + +export interface DragItem> { + type: string; + data: T; +} + +export interface DndContextDescriptor { + dragging: DragItem | null, +} + +const defaultDndContext: DndContextDescriptor = proxy({ + dragging: null, +}); + +export const DndContext = createContext(defaultDndContext); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drag.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drag.hooks.ts new file mode 100644 index 0000000000..da5c840597 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drag.hooks.ts @@ -0,0 +1,107 @@ +import { + DragEventHandler, + useCallback, + useContext, + useMemo, + useRef, + useState, +} from 'react'; +import { DndContext } from './dnd.context'; +import { autoScrollOnEdge, EdgeGap, getScrollParent, ScrollDirection } from './utils'; + +export interface UseDraggableOptions { + type: string; + effectAllowed?: DataTransfer['effectAllowed']; + data?: Record; + disabled?: boolean; + scrollOnEdge?: { + direction?: ScrollDirection; + edgeGap?: number | Partial; + }; +} + +export const useDraggable = ({ + type, + effectAllowed = 'copyMove', + data, + disabled, + scrollOnEdge, +}: UseDraggableOptions) => { + const scrollDirection = scrollOnEdge?.direction; + const edgeGap = scrollOnEdge?.edgeGap; + + const context = useContext(DndContext); + const typeRef = useRef(type); + const dataRef = useRef(data); + const previewRef = useRef(null); + const [ isDragging, setIsDragging ] = useState(false); + + typeRef.current = type; + dataRef.current = data; + + const setPreviewRef = useCallback((previewElement: null | Element) => { + previewRef.current = previewElement; + }, []); + + const attributes = useMemo(() => { + if (disabled) { + return {}; + } + + return { + draggable: true, + }; + }, [disabled]); + + const onDragStart = useCallback((event) => { + setIsDragging(true); + context.dragging = { + type: typeRef.current, + data: dataRef.current ?? {}, + }; + + const { dataTransfer } = event; + const previewNode = previewRef.current; + + dataTransfer.effectAllowed = effectAllowed; + + if (previewNode) { + const { clientX, clientY } = event; + const rect = previewNode.getBoundingClientRect(); + + dataTransfer.setDragImage(previewNode, clientX - rect.x, clientY - rect.y); + } + + if (scrollDirection === undefined) { + return; + } + + const scrollParent: HTMLElement | null = getScrollParent(event.target as HTMLElement, scrollDirection); + + if (scrollParent) { + autoScrollOnEdge({ + element: scrollParent, + direction: scrollDirection, + edgeGap, + }); + } + }, [ context, effectAllowed, scrollDirection, edgeGap ]); + + const onDragEnd = useCallback(() => { + setIsDragging(false); + context.dragging = null; + }, [ context ]); + + const listeners = useMemo(() => ({ + onDragStart, + onDragEnd, + }), [ onDragStart, onDragEnd]); + + return { + isDragging, + previewRef, + attributes, + listeners, + setPreviewRef, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drop.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drop.hooks.ts new file mode 100644 index 0000000000..7b3d79aeb2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drop.hooks.ts @@ -0,0 +1,88 @@ +import { DragEventHandler, useContext, useState, useMemo, useCallback } from 'react'; +import { useSnapshot } from 'valtio'; +import { DragItem, DndContext } from './dnd.context'; + +interface UseDroppableOptions { + accept: string; + dropEffect?: DataTransfer['dropEffect']; + disabled?: boolean; + onDragOver?: DragEventHandler, + onDrop?: (data: DragItem) => void; +} + +export const useDroppable = ({ + accept, + dropEffect = 'move', + disabled, + onDragOver: handleDragOver, + onDrop: handleDrop, +}: UseDroppableOptions) => { + const dndContext = useContext(DndContext); + const dndSnapshot = useSnapshot(dndContext); + + const [ dragOver, setDragOver ] = useState(false); + const canDrop = useMemo( + () => !disabled && dndSnapshot.dragging?.type === accept, + [ disabled, accept, dndSnapshot.dragging?.type ], + ); + const isOver = useMemo(()=> canDrop && dragOver, [ canDrop, dragOver ]); + + const onDragEnter = useCallback((event) => { + if (!canDrop) { + return; + } + + event.preventDefault(); + event.dataTransfer.dropEffect = dropEffect; + + setDragOver(true); + }, [ canDrop, dropEffect ]); + + const onDragOver = useCallback((event) => { + if (!canDrop) { + return; + } + + event.preventDefault(); + event.dataTransfer.dropEffect = dropEffect; + + setDragOver(true); + handleDragOver?.(event); + }, [ canDrop, dropEffect, handleDragOver ]); + + const onDragLeave = useCallback(() => { + if (!canDrop) { + return; + } + + setDragOver(false); + }, [ canDrop ]); + + const onDrop = useCallback(() => { + if (!canDrop) { + return; + } + + const dragging = dndSnapshot.dragging; + + if (!dragging) { + return; + } + + setDragOver(false); + handleDrop?.(dragging); + }, [ canDrop, dndSnapshot.dragging, handleDrop ]); + + const listeners = useMemo(() => ({ + onDragEnter, + onDragOver, + onDragLeave, + onDrop, + }), [ onDragEnter, onDragOver, onDragLeave, onDrop ]); + + return { + isOver, + canDrop, + listeners, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/index.ts new file mode 100644 index 0000000000..8688534359 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/index.ts @@ -0,0 +1,7 @@ +export * from './dnd.context'; +export * from './drag.hooks'; +export * from './drop.hooks'; +export { + ScrollDirection, + Edge, +} from './utils'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/utils.ts new file mode 100644 index 0000000000..562af3cbef --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/utils.ts @@ -0,0 +1,180 @@ +import { interval } from '$app/utils/tool'; + +export enum Edge { + Top = 'top', + Bottom = 'bottom', + Left = 'left', + Right = 'right', +} + +export enum ScrollDirection { + Horizontal = 'horizontal', + Vertical = 'vertical', +} + +export interface EdgeGap { + top: number; + bottom: number; + left: number; + right: number; +} + +export const isReachEdge = (element: Element, edge: Edge) => { + switch (edge) { + case Edge.Left: + return element.scrollLeft === 0; + case Edge.Right: + return element.scrollLeft + element.clientWidth === element.scrollWidth; + case Edge.Top: + return element.scrollTop === 0; + case Edge.Bottom: + return element.scrollTop + element.clientHeight === element.scrollHeight; + default: + return true; + } +}; + +export const scrollBy = (element: Element, edge: Edge, offset: number) => { + let step = offset; + let prop = edge; + + if (edge === Edge.Left || edge === Edge.Top) { + step = -offset; + } else if (edge === Edge.Right) { + prop = Edge.Left; + } else if (edge === Edge.Bottom) { + prop = Edge.Top; + } + + element.scrollBy({ [prop]: step }); +}; + +export const scrollElement = (element: Element, edge: Edge, offset: number) => { + if (isReachEdge(element, edge)) { + return; + } + + scrollBy(element, edge, offset); +}; + +export const calculateLeaveEdge = ( + { x: mouseX, y: mouseY }: { x: number; y: number }, + rect: DOMRect, + gaps: EdgeGap, + direction: ScrollDirection, +) => { + if (direction === ScrollDirection.Horizontal) { + if (mouseX - rect.left < gaps.left) { + return Edge.Left; + } + + if (rect.right - mouseX < gaps.right) { + return Edge.Right; + } + } + + if (direction === ScrollDirection.Vertical) { + if (mouseY - rect.top < gaps.top) { + return Edge.Top; + } + + if (rect.bottom - mouseY < gaps.bottom) { + return Edge.Bottom; + } + } + + return null; +}; + +export const getScrollParent = (element: HTMLElement | null, direction: ScrollDirection): HTMLElement | null => { + if (element === null) { + return null; + } + + if (direction === ScrollDirection.Horizontal && element.scrollWidth > element.clientWidth) { + return element; + } + + if (direction === ScrollDirection.Vertical && element.scrollHeight > element.clientHeight) { + return element; + } + + return getScrollParent(element.parentElement, direction); +}; + +export interface AutoScrollOnEdgeOptions { + element: HTMLElement; + direction: ScrollDirection; + edgeGap?: number | Partial; + step?: number; +} + +const defaultEdgeGap = 30; + +export const autoScrollOnEdge = ({ + element, + direction, + edgeGap, + step = 8, +}: AutoScrollOnEdgeOptions) => { + const gaps = typeof edgeGap === 'number' + ? { + top: edgeGap, + bottom: edgeGap, + left: edgeGap, + right: edgeGap, + } + : { + top: defaultEdgeGap, + bottom: defaultEdgeGap, + left: defaultEdgeGap, + right: defaultEdgeGap, + ...edgeGap, + }; + + const keepScroll = interval(scrollElement, 8); + + let leaveEdge: Edge | null = null; + + const onDragOver = (event: DragEvent) => { + const rect = element.getBoundingClientRect(); + + leaveEdge = calculateLeaveEdge( + { x: event.clientX, y: event.clientY }, + rect, + gaps, + direction, + ); + + if (leaveEdge) { + keepScroll(element, leaveEdge, step); + } else { + keepScroll.cancel(); + } + }; + + const onDragLeave = () => { + if (!leaveEdge) { + return; + } + + keepScroll(element, leaveEdge, step * 2); + }; + + const cleanup = () => { + console.log('document drag end'); + keepScroll.cancel(); + + element.removeEventListener('dragover', onDragOver); + element.removeEventListener('dragleave', onDragLeave); + + document.removeEventListener('dragend', cleanup); + }; + + element.addEventListener('dragover', onDragOver); + element.addEventListener('dragleave', onDragLeave); + + document.addEventListener('dragend', cleanup); + + return cleanup; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/index.ts new file mode 100644 index 0000000000..8d63e9f3ce --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/index.ts @@ -0,0 +1,6 @@ +export * from './constants'; + +export * from './dnd'; + +export * from './VirtualizedList'; +export * from './CellText'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/database.context.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/database.context.ts index 912eb25e45..7ce61fd06d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/database.context.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/database.context.ts @@ -1,7 +1,7 @@ -import { Database } from '$app/interfaces/database'; -import { DatabaseLayoutPB } from '@/services/backend'; import { RefObject, createContext, createRef } from 'react'; import { proxy } from 'valtio'; +import { Database } from '$app/interfaces/database'; +import { DatabaseLayoutPB } from '@/services/backend'; export const VerticalScrollElementRefContext = createContext>(createRef()); export const DatabaseContext = createContext(proxy({ diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/database_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/database_bd_svc.ts index 33dcbfdb63..48fed99cbe 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/database_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/database_bd_svc.ts @@ -48,6 +48,7 @@ import { DatabaseSnapshotPB, FilterPB, SortPB, + MoveRowPayloadPB, } from '@/services/backend'; import { DatabaseEventGetDatabases, @@ -92,6 +93,7 @@ import { DatabaseEventGetDatabaseSnapshots, DatabaseEventGetAllFilters, DatabaseEventGetAllSorts, + DatabaseEventMoveRow, } from "@/services/backend/events/flowy-database2"; export async function getDatabases(): Promise { @@ -164,7 +166,7 @@ export async function setLayoutSetting(viewId: string, setting: { show_week_numbers: setting.calendar.showWeekNumbers, } : undefined, }); - + const result = await DatabaseEventSetLayoutSetting(payload); return result.unwrap(); @@ -493,13 +495,25 @@ export async function deleteRow(viewId: string, rowId: string, groupId?: string) return result.unwrap(); } +export async function moveRow(viewId: string, fromRowId: string, toRowId: string): Promise { + const payload = MoveRowPayloadPB.fromObject({ + view_id: viewId, + from_row_id: fromRowId, + to_row_id: toRowId, + }); + + const result = await DatabaseEventMoveRow(payload); + + return result.unwrap(); +} + /** * Move the row from one group to another group * - * @param fromRowId - * @param toGroupId + * @param fromRowId + * @param toGroupId * @param toRowId used to locate the moving row location. - * @returns + * @returns */ export async function moveGroupRow(viewId: string, fromRowId: string, toGroupId: string, toRowId?: string): Promise { const payload = MoveGroupRowPayloadPB.fromObject({ @@ -671,7 +685,7 @@ export async function updateDateCell( }); const result = await DatabaseEventUpdateDateCell(payload); - + return result.unwrap(); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridCell.tsx index 5f15ffc5c5..08e002590d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridCell.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridCell.tsx @@ -34,5 +34,5 @@ export const GridCell: FC = ({ }, [field.type]); // TODO: find a better way to check cell type. - return -}; \ No newline at end of file + return ; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridCheckboxCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridCheckboxCell.tsx index 8b584c16da..75a177825e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridCheckboxCell.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridCheckboxCell.tsx @@ -17,7 +17,7 @@ export const GridCheckboxCell: FC<{ }, [viewId, rowId, field.id ]); return ( -
+
); -}; \ No newline at end of file +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridTextCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridTextCell.tsx index 4d5ae04259..3ff3902582 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridTextCell.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridTextCell.tsx @@ -3,6 +3,7 @@ import { FC, FormEventHandler, useCallback, useEffect, useRef, useState } from ' import { Database } from '$app/interfaces/database'; import * as service from '$app/components/database/database_bd_svc'; import { useViewId } from '../../database.hooks'; +import { CellText } from '../../_shared'; export const GridTextCell: FC<{ rowId: string; @@ -42,19 +43,19 @@ export const GridTextCell: FC<{ return ( <> -
{cell?.data} -
+ {editing && ( = ({ field }) => { - const anchorEl = useRef(null); - const [open, setOpen] = useState(false); + const viewId = useViewId(); + const { fields } = useDatabase(); + const [ openMenu, setOpenMenu ] = useState(false); + const [ openTooltip, setOpenTooltip ] = useState(false); + const [ dropPosition, setDropPosition ] = useState(DropPosition.Before); const handleClick = useCallback(() => { - setOpen(true); + setOpenMenu(true); }, []); - const handleClose = useCallback(() => { - setOpen(false); + const handleMenuClose = useCallback(() => { + setOpenMenu(false); }, []); + const handleTooltipOpen = useCallback(() => { + setOpenTooltip(true); + }, []); + + const handleTooltipClose = useCallback(() => { + setOpenTooltip(false); + }, []); + + const draggingData = useMemo(() => ({ + field, + }), [field]); + + const { + isDragging, + attributes, + listeners, + setPreviewRef, + previewRef, + } = useDraggable({ + type: DragType.Field, + data: draggingData, + scrollOnEdge: { + direction: ScrollDirection.Horizontal, + }, + }); + + const onDragOver = useMemo(() => { + return throttle((event) => { + const element = previewRef.current; + + if (!element) { + return; + } + + const { left, right } = element.getBoundingClientRect(); + const middle = (left + right) / 2; + + setDropPosition(event.clientX < middle ? DropPosition.Before : DropPosition.After); + }, 20); + }, [previewRef]); + + const onDrop = useCallback(({ data }: DragItem) => { + const dragField = data.field as Database.Field; + const fromIndex = fields.findIndex(item => item.id === dragField.id); + const dropIndex = fields.findIndex(item => item.id === field.id); + const toIndex = dropIndex + dropPosition + (fromIndex < dropIndex ? -1 : 0); + + if (fromIndex === toIndex) { + return; + } + + void service.moveField(viewId, dragField.id, fromIndex, toIndex); + }, [viewId, field, fields, dropPosition]); + + const { + isOver, + listeners: dropListeners, + } = useDroppable({ + accept: DragType.Field, + disabled: isDragging, + onDragOver, + onDrop, + }); + return ( -
-
- - - {field.name} - -
- - - - -
+ <> + + + + {openMenu && ( + + )} + ); -}; \ No newline at end of file +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCalculateRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCalculateRow.tsx new file mode 100644 index 0000000000..871958e74e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCalculateRow.tsx @@ -0,0 +1,3 @@ +export const GridCalculateRow = () => { + return null; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow.tsx index 46fa823195..037d38074e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow.tsx @@ -1,36 +1,140 @@ +import { Virtualizer } from '@tanstack/react-virtual'; +import { IconButton, Tooltip } from '@mui/material'; +import { t } from 'i18next'; +import { DragEventHandler, FC, useCallback, useMemo, useState } from 'react'; import { Database } from '$app/interfaces/database'; -import { VirtualItem } from '@tanstack/react-virtual'; -import { FC } from 'react'; -import { useDatabase } from '../../database.hooks'; +import { ReactComponent as DragSvg } from '$app/assets/drag.svg'; +import { throttle } from '$app/utils/tool'; +import { useDatabase, useViewId } from '../../database.hooks'; +import * as service from '../../database_bd_svc'; +import { DragItem, DragType, DropPosition, VirtualizedList, useDraggable, useDroppable, ScrollDirection } from '../../_shared'; import { GridCell } from '../GridCell'; +import { GridCellRowActions } from './GridCellRowActions'; -export const GridCellRow: FC<{ - columnVirtualItems: VirtualItem[]; +export interface GridCellRowProps { row: Database.Row; - before: number; - after: number; -}> = ({ columnVirtualItems, row, before, after }) => { + virtualizer: Virtualizer; +} + +export const GridCellRow: FC = ({ + row, + virtualizer, +}) => { + const viewId = useViewId(); const { fields } = useDatabase(); + const [ hover, setHover ] = useState(false); + const [ openTooltip, setOpenTooltip ] = useState(false); + const [ dropPosition, setDropPosition ] = useState(DropPosition.Before); + + const handleMouseEnter = useCallback(() => { + setHover(true); + }, []); + + const handleMouseLeave = useCallback(() => { + setHover(false); + }, []); + + const handleTooltipOpen = useCallback(() => { + setOpenTooltip(true); + }, []); + + const handleTooltipClose = useCallback(() => { + setOpenTooltip(false); + }, []); + + const dragData = useMemo(() => ({ + row, + }), [row]); + + const { + isDragging, + attributes, + listeners, + setPreviewRef, + previewRef, + } = useDraggable({ + type: DragType.Row, + data: dragData, + scrollOnEdge: { + direction: ScrollDirection.Vertical, + }, + }); + + const onDragOver = useMemo(() => { + return throttle((event) => { + const element = previewRef.current; + + if (!element) { + return; + } + + const { top, bottom } = element.getBoundingClientRect(); + const middle = (top + bottom) / 2; + + setDropPosition(event.clientY < middle ? DropPosition.Before : DropPosition.After); + }, 20); + }, [previewRef]); + + const onDrop = useCallback(({ data }: DragItem) => { + void service.moveRow(viewId, (data.row as Database.Row).id, row.id); + }, [viewId, row.id]); + + const { + isOver, + listeners: dropListeners, + } = useDroppable({ + accept: DragType.Row, + disabled: isDragging, + onDragOver, + onDrop, + }); + return ( - <> -
- {before > 0 &&
} - {columnVirtualItems.map(virtualColumn => ( -
+ + + - -
- ))} - {after > 0 &&
} + + + + +
+ ( + + )} + /> +
+ {isOver &&
}
-
- +
); -} \ No newline at end of file +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRowActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRowActions.tsx new file mode 100644 index 0000000000..65de7de4b2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRowActions.tsx @@ -0,0 +1,36 @@ +import { IconButton, Tooltip } from '@mui/material'; +import { FC, PropsWithChildren, useCallback } from 'react'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import * as service from '$app/components/database/database_bd_svc'; +import { useViewId } from '../../database.hooks'; +import { t } from 'i18next'; + +export interface GridCellRowActionsProps { + className?: string; + rowId: string; +} + +export const GridCellRowActions: FC> = ({ + className, + rowId, + children, +}) => { + const viewId = useViewId(); + + const handleInsertRowClick = useCallback(() => { + void service.createRow(viewId, { + startRowId: rowId, + }); + }, [viewId, rowId]); + + return ( +
+ + + + + + {children} +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx index 1f2690a717..04df6e283b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx @@ -1,57 +1,41 @@ -import { VirtualItem } from '@tanstack/react-virtual'; -import { t } from 'i18next'; +import { Virtualizer } from '@tanstack/react-virtual'; import { FC } from 'react'; import { Button } from '@mui/material'; import { FieldType } from '@/services/backend'; import { ReactComponent as AddSvg } from '$app/assets/add.svg'; import * as service from '$app/components/database/database_bd_svc'; import { useDatabase } from '../../database.hooks'; +import { VirtualizedList } from '../../_shared'; import { GridField } from '../GridField'; -export const GridFieldRow: FC<{ - columnVirtualItems: VirtualItem[]; - before: number; - after: number; -}> = ({ columnVirtualItems, before, after }) => { +export interface GridFieldRowProps { + virtualizer: Virtualizer; +} + +export const GridFieldRow: FC = ({ + virtualizer, +}) => { const { viewId, fields } = useDatabase(); const handleClick = async () => { await service.createFieldTypeOption(viewId, FieldType.RichText); }; return ( - <> -
- {before > 0 &&
} - {columnVirtualItems.map(virtualColumn => ( -
- -
- ))} - {after > 0 &&
} -
-
+
+ } + /> +
+ />
- +
); -} \ No newline at end of file +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridNewRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridNewRow.tsx index 971fe15f98..cf9dd8788b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridNewRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridNewRow.tsx @@ -1,5 +1,6 @@ import { useCallback } from 'react'; import { t } from 'i18next'; +import { Button } from '@mui/material'; import { ReactComponent as AddSvg } from '$app/assets/add.svg'; import * as service from '$app/components/database/database_bd_svc'; import { useDatabase, useViewId } from '../../database.hooks'; @@ -16,14 +17,16 @@ export const GridNewRow = () => { }, [viewId, lastRowId]); return ( -
- - - {t('grid.row.newRow')} - +
+
); -}; \ No newline at end of file +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx index 1d7d9fda52..a1749451d3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx @@ -1,38 +1,36 @@ -import { VirtualItem } from '@tanstack/react-virtual'; +import { Virtualizer } from '@tanstack/react-virtual'; import { FC } from 'react'; import { RenderRow, RenderRowType } from './constants'; import { GridCellRow } from './GridCellRow'; import { GridFieldRow } from './GridFieldRow'; import { GridNewRow } from './GridNewRow'; +import { GridCalculateRow } from './GridCalculateRow'; -export const GridRow: FC<{ +export interface GridRowProps { row: RenderRow; - columnVirtualItems: VirtualItem[]; - before: number; - after: number; -}> = ({ row, columnVirtualItems, before, after }) => { + virtualizer: Virtualizer; +} + +export const GridRow: FC = ({ + row, + virtualizer, +}) => { switch (row.type) { case RenderRowType.Row: return ( ); case RenderRowType.Fields: - return ( - - ); + return ; case RenderRowType.NewRow: return ; + case RenderRowType.Calculate: + return ; default: return null; } -}; \ No newline at end of file +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts index 286d51ae10..e8ae30c1dd 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts @@ -4,6 +4,7 @@ export enum RenderRowType { Fields = 'fields', Row = 'row', NewRow = 'new-row', + Calculate = 'calculate', } export interface FieldRenderRow { @@ -19,4 +20,8 @@ export interface NewRenderRow { type: RenderRowType.NewRow; } -export type RenderRow = FieldRenderRow | CellRenderRow | NewRenderRow; \ No newline at end of file +export interface CalculateRenderRow { + type: RenderRowType.Calculate; +} + +export type RenderRow = FieldRenderRow | CellRenderRow | NewRenderRow | CalculateRenderRow; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.tsx index b8f94065a6..587e2ec8ea 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.tsx @@ -1,26 +1,32 @@ -import { Virtualizer, useVirtualizer } from '@tanstack/react-virtual'; +import { useVirtualizer } from '@tanstack/react-virtual'; import { FC, useContext, useMemo, useRef } from 'react'; import { VerticalScrollElementRefContext } from '../../database.context'; import { useDatabase } from '../../database.hooks'; +import { VirtualizedList } from '../../_shared'; import { GridRow, RenderRow, RenderRowType } from '../GridRow'; -import { VirtualizedRows } from './VirtualizedRows'; -const calculateBeforeAfter = (columnVirtualizer: Virtualizer) => { - const columnVirtualItems = columnVirtualizer.getVirtualItems(); +const getRenderRowKey = (row: RenderRow) => { + if (row.type === RenderRowType.Row) { + return `row:${row.data.id}`; + } - return columnVirtualItems.length > 0 - ? [ - columnVirtualItems[0].start, - columnVirtualizer.getTotalSize() - columnVirtualItems[columnVirtualItems.length - 1].end, - ] - : [0, 0]; + return row.type; +}; + +const getRenderRowHeight = (row: RenderRow) => { + const defaultRowHeight = 37; + + if (row.type === RenderRowType.Row) { + return row.data.height ?? defaultRowHeight; + } + + return defaultRowHeight; }; export const GridTable: FC = () => { const verticalScrollElementRef = useContext(VerticalScrollElementRefContext); - const { rows, fields } = useDatabase(); - const horizontalScrollElementRef = useRef(null); + const { rows, fields } = useDatabase(); const renderRows = useMemo(() => { return [ @@ -34,12 +40,22 @@ export const GridTable: FC = () => { { type: RenderRowType.NewRow, }, + { + type: RenderRowType.Calculate, + }, ]; }, [rows]); - const defaultColumnWidth = 221; + const rowVirtualizer = useVirtualizer({ + count: renderRows.length, + overscan: 10, + getItemKey: i => getRenderRowKey(renderRows[i]), + getScrollElement: () => verticalScrollElementRef.current, + estimateSize: i => getRenderRowHeight(renderRows[i]), + }); - const columnVirtualizer = useVirtualizer({ + const defaultColumnWidth = 221; + const columnVirtualizer = useVirtualizer({ horizontal: true, count: fields.length, overscan: 5, @@ -48,28 +64,20 @@ export const GridTable: FC = () => { estimateSize: (i) => fields[i].width ?? defaultColumnWidth, }); - const columnVirtualItems = columnVirtualizer.getVirtualItems(); - const [before, after] = calculateBeforeAfter(columnVirtualizer); - return (
-
- ( - - )} - /> -
+ ( + + )} + />
); -}; \ No newline at end of file +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/VirtualizedRows.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/VirtualizedRows.tsx deleted file mode 100644 index decbe3d893..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/VirtualizedRows.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useVirtualizer } from '@tanstack/react-virtual'; -import { FC, RefObject } from 'react'; -import { RenderRow, RenderRowType } from '../GridRow'; - -export interface VirtualizedRowsProps { - rows: RenderRow[]; - scrollElementRef: RefObject; - defaultHeight?: number; - renderRow: (row: RenderRow, index: number) => React.ReactNode; -} - -const getRenderRowKey = (row: RenderRow) => { - switch (row.type) { - case RenderRowType.Row: - return `row:${row.data.id}`; - case RenderRowType.Fields: - return 'fields'; - case RenderRowType.NewRow: - return 'new-row'; - default: - return ''; - } -}; - -const getRenderRowHeight = (row: RenderRow) => { - switch (row.type) { - case RenderRowType.Row: - return row.data.height ?? 41; - case RenderRowType.Fields: - return 41; - case RenderRowType.NewRow: - return 36; - default: - return 0; - } -}; - -export const VirtualizedRows: FC = ({ - rows, - scrollElementRef, - renderRow, -}) => { - const virtualizer = useVirtualizer({ - count: rows.length, - overscan: 5, - getItemKey: i => getRenderRowKey(rows[i]), - getScrollElement: () => scrollElementRef.current, - estimateSize: i => getRenderRowHeight(rows[i]), - }); - - const virtualItems = virtualizer.getVirtualItems(); - - return ( -
- {virtualItems.map((virtualRow) => { - return ( -
- {renderRow(rows[virtualRow.index], virtualRow.index)} -
- ); - })} -
- ); -}; \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/index.ts index 46e146003f..fafeee00d4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/index.ts @@ -1,3 +1,3 @@ export * from './database.context'; export * from './database.hooks'; -export * from './grid'; \ No newline at end of file +export * from './Database'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts index c53911c52f..de291b980d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts @@ -14,10 +14,14 @@ export function debounce(fn: (...args: any[]) => void, delay: number) { return debounceFn; } -export function throttle(fn: (...args: any[]) => void, delay: number, immediate = true) { +export function throttle void = (...args: any[]) => void>( + fn: T, + delay: number, + immediate = true, +): T { let timeout: NodeJS.Timeout | null = null; - return (...args: any[]) => { + const run = (...args: Parameters) => { if (!timeout) { timeout = setTimeout(() => { timeout = null; @@ -26,6 +30,8 @@ export function throttle(fn: (...args: any[]) => void, delay: number, immediate immediate && fn.apply(undefined, args); } }; + + return run as T; } export function get(obj: any, path: string[], defaultValue?: any): T { @@ -126,3 +132,59 @@ export function chunkArray(array: T[], chunkSize: number) { return chunks; } + +/** + * Creates an interval that repeatedly calls the given function with a specified delay. + * + * @param {Function} fn - The function to be called repeatedly. + * @param {number} [delay] - The delay between function calls in milliseconds. + * @param {Object} [options] - Additional options for the interval. + * @param {boolean} [options.immediate] - Whether to immediately call the function when the interval is created. Default is true. + * + * @return {Function} - The function that runs the interval. + * @return {Function.cancel} - A method to cancel the interval. + * + * @example + * const log = interval((message) => console.log(message), 1000); + * + * log('foo'); // prints 'foo' every second. + * + * log('bar'); // change to prints 'bar' every second. + * + * log.cancel(); // stops the interval. + */ +export function interval any = (...args: any[]) => any>( + fn: T, + delay?: number, + options?: { immediate?: boolean }, +): T & { cancel: () => void } { + const { immediate = true } = options || {}; + let intervalId: NodeJS.Timer | null = null; + let parameters: any[] = []; + + function run(...args: Parameters) { + parameters = args; + + if (intervalId !== null) { + return; + } + + immediate && fn.apply(undefined, parameters); + intervalId = setInterval(() => { + fn.apply(undefined, parameters); + }, delay); + } + + function cancel() { + if (intervalId === null) { + return; + } + + clearInterval(intervalId); + intervalId = null; + parameters = []; + } + + run.cancel = cancel; + return run as T & { cancel: () => void }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx index 2da3cc00a5..61f86d22f0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx @@ -1,38 +1,5 @@ -import { useRef } from 'react'; -import { useSnapshot } from 'valtio'; -import { DatabaseLayoutPB } from '@/services/backend'; -import { - VerticalScrollElementRefContext, - DatabaseContext, - Grid, - useViewId, - useConnectDatabase, -} from '../components/database'; +import { Database } from '../components/database'; export const DatabasePage = () => { - const viewId = useViewId(); - const scrollElementRef = useRef(null); - const database = useConnectDatabase(viewId); - const snapshot = useSnapshot(database); - - return ( -
-
-
-

Grid

-
- 👋 Welcome to AppFlowy -
-
- - - {snapshot.layoutType === DatabaseLayoutPB.Grid ? : null} - - -
-
- ); + return ; }; diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index eb3a740dc7..6b1cd97c22 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -351,6 +351,9 @@ "grid": { "deleteView": "Are you sure you want to delete this view?", "createView": "New", + "title": { + "placeholder": "Untitled" + }, "settings": { "filter": "Filter", "sort": "Sort", @@ -457,7 +460,9 @@ "copyProperty": "Copied property to clipboard", "count": "Count", "newRow": "New row", - "action": "Action" + "action": "Action", + "add": "Click add to below", + "drag": "Drag to move" }, "selectOption": { "create": "Create",