From d765806337a148b425712913fca02eae82f85397 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:54:41 +0800 Subject: [PATCH] feat: parity features for flutter grid (#4082) * feat: parity features of flutter grid * feat: replace another virtual scroll component * fix: fix eslint error * fix: modify the drag style * fix: remove log * fix: add css style for row when context menu display --- frontend/appflowy_tauri/.eslintrc.cjs | 2 +- frontend/appflowy_tauri/package.json | 4 + frontend/appflowy_tauri/pnpm-lock.yaml | 63 +++- .../components/database/Database.hooks.ts | 69 ++--- .../components/database/Database.tsx | 32 +- .../components/database/DatabaseView.tsx | 3 +- .../components/database/_shared/CellText.tsx | 4 +- .../database/_shared/dnd/drag.hooks.ts | 6 +- .../application/field/field_service.ts | 26 +- .../database/components/cell/Cell.hooks.ts | 2 +- .../database/components/cell/Cell.tsx | 11 +- .../database/components/cell/CheckboxCell.tsx | 5 +- .../database/components/cell/ExpandButton.tsx | 43 --- .../database/components/cell/NumberCell.tsx | 2 +- .../database/components/cell/TextCell.tsx | 47 +-- .../database/components/cell/URLCell.tsx | 3 +- .../database_settings/Properties.tsx | 98 ++++-- .../components/edit_record/EditRecord.tsx | 22 +- .../ExpandRecordModal.tsx} | 19 +- .../components/edit_record/RecordActions.tsx | 7 +- .../components/edit_record/RecordHeader.tsx | 11 +- .../components/edit_record/RecordTitle.tsx | 28 +- .../record_properties/NewProperty.tsx | 54 ---- .../record_properties/Property.tsx | 38 ++- .../record_properties/PropertyActions.tsx | 20 -- .../record_properties/PropertyList.tsx | 15 +- .../record_properties/PropertyName.tsx | 16 +- .../record_properties/RecordProperties.tsx | 63 ++-- .../SwitchPropertiesVisible.tsx | 38 +++ .../database/components/field/Field.tsx | 16 - .../database/components/field/FieldMenu.tsx | 92 ------ .../components/field/FieldMenuActions.tsx | 108 ------- .../database/components/field/index.ts | 4 - .../field_types/text/EditTextCellInput.tsx | 5 +- .../database/components/filter/Filter.tsx | 4 +- .../components/filter/FilterFieldsMenu.tsx | 4 +- .../components/property/NewProperty.tsx | 36 +++ .../PropertiesList.tsx} | 8 +- .../database/components/property/Property.tsx | 61 ++++ .../components/property/PropertyActions.tsx | 143 +++++++++ .../components/property/PropertyMenu.tsx | 72 +++++ .../components/property/PropertyNameInput.tsx | 47 +++ .../PropertySelect.tsx} | 6 +- .../database/components/property/index.ts | 4 + .../property_type/PropertyTypeMenu.tsx} | 8 +- .../PropertyTypeMenuExtension.tsx} | 4 +- .../property_type/PropertyTypeSelect.tsx} | 16 +- .../property_type/PropertyTypeText.tsx} | 2 +- .../property_type/ProppertyTypeSvg.tsx} | 2 +- .../components/sort/SortFieldsMenu.tsx | 4 +- .../database/components/sort/SortItem.tsx | 4 +- .../database/components/tab_bar/ViewTabs.tsx | 3 +- .../components/database/grid/Grid/Grid.tsx | 11 +- .../grid/GridCalculate/GridCalculate.tsx | 7 +- .../database/grid/GridCell/GridCell.tsx | 77 ++++- .../database/grid/GridCell/PrimaryCell.tsx | 51 ++++ .../database/grid/GridField/GridField.tsx | 278 +++++++++++------- .../database/grid/GridField/GridFieldMenu.tsx | 47 +++ .../database/grid/GridField/GridNewField.tsx | 35 +++ .../database/grid/GridField/GridResizer.tsx | 7 +- .../database/grid/GridNewRow/GridNewRow.tsx | 69 +++++ .../grid/GridOverlay/GridTableOverlay.tsx | 44 +++ .../grid/GridRow/GridCalculateRow.tsx | 17 -- .../GridRow/GridCellRow/GridCellRow.hooks.ts | 56 ---- .../grid/GridRow/GridCellRow/GridCellRow.tsx | 165 ----------- .../GridCellRow/GridCellRowActions.tsx | 97 ------ .../GridCellRow/GridCellRowContextMenu.tsx | 36 --- .../grid/GridRow/GridCellRow/index.ts | 1 - .../database/grid/GridRow/GridFieldRow.tsx | 25 -- .../database/grid/GridRow/GridNewRow.tsx | 33 --- .../database/grid/GridRow/GridRow.tsx | 28 -- .../components/database/grid/GridRow/index.ts | 2 - .../GridRowActions/GridRowActions.hooks.ts | 219 ++++++++++++++ .../grid/GridRowActions/GridRowActions.tsx | 98 ++++++ .../GridRowActions/GridRowContextMenu.tsx | 64 ++++ .../grid/GridRowActions/GridRowDragButton.tsx | 36 +++ .../GridRowMenu.tsx} | 63 ++-- .../GridStickyHeader.hooks.ts | 9 + .../GridStickyHeader/GridStickyHeader.tsx | 102 +++++++ .../grid/GridTable/GridTable.hooks.ts | 58 ++++ .../database/grid/GridTable/GridTable.tsx | 161 ++++++---- .../database/grid/{GridRow => }/constants.ts | 35 ++- .../database/proxy/grid/ui_state/Provider.tsx | 14 - .../database/proxy/grid/ui_state/actions.ts | 46 --- .../appflowy_tauri/src/styles/template.css | 3 + 85 files changed, 2042 insertions(+), 1356 deletions(-) delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/ExpandButton.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/database/components/{cell/expand_type/ExpandCellModal.tsx => edit_record/ExpandRecordModal.tsx} (76%) delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/NewProperty.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyActions.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/SwitchPropertiesVisible.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/Field.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldMenu.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldMenuActions.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/NewProperty.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/database/components/{field/FieldList.tsx => property/PropertiesList.tsx} (88%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/Property.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyActions.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyMenu.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyNameInput.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/database/components/{field/FieldSelect.tsx => property/PropertySelect.tsx} (87%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/index.ts rename frontend/appflowy_tauri/src/appflowy_app/components/database/components/{field/FieldTypeMenu.tsx => property/property_type/PropertyTypeMenu.tsx} (86%) rename frontend/appflowy_tauri/src/appflowy_app/components/database/components/{field/FieldTypeMenuExtension.tsx => property/property_type/PropertyTypeMenuExtension.tsx} (90%) rename frontend/appflowy_tauri/src/appflowy_app/components/database/components/{field/FieldTypeSelect.tsx => property/property_type/PropertyTypeSelect.tsx} (68%) rename frontend/appflowy_tauri/src/appflowy_app/components/database/components/{field/FieldTypeText.tsx => property/property_type/PropertyTypeText.tsx} (93%) rename frontend/appflowy_tauri/src/appflowy_app/components/database/components/{field/FieldTypeSvg.tsx => property/property_type/ProppertyTypeSvg.tsx} (93%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/PrimaryCell.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridFieldMenu.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridNewField.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridNewRow/GridNewRow.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridOverlay/GridTableOverlay.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCalculateRow.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowActions.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/index.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridNewRow.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowActions.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowActions.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowContextMenu.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowDragButton.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/database/grid/{GridRow/GridCellRow/GridCellRowMenu.tsx => GridRowActions/GridRowMenu.tsx} (61%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridStickyHeader/GridStickyHeader.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridStickyHeader/GridStickyHeader.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.hooks.ts rename frontend/appflowy_tauri/src/appflowy_app/components/database/grid/{GridRow => }/constants.ts (62%) delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/Provider.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/actions.ts diff --git a/frontend/appflowy_tauri/.eslintrc.cjs b/frontend/appflowy_tauri/.eslintrc.cjs index 2047d4f962..430fe259d7 100644 --- a/frontend/appflowy_tauri/.eslintrc.cjs +++ b/frontend/appflowy_tauri/.eslintrc.cjs @@ -68,5 +68,5 @@ module.exports = { ] }, - ignorePatterns: ['src/**/*.test.ts'], + ignorePatterns: ['src/**/*.test.ts', 'package.json'], }; diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index 7612662050..a42c3691fe 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -61,6 +61,9 @@ "react-router-dom": "^6.8.0", "react-swipeable-views": "^0.14.0", "react-transition-group": "^4.4.5", + "react-virtualized-auto-sizer": "^1.0.20", + "react-vtree": "^2.0.4", + "react-window": "^1.8.10", "react18-input-otp": "^1.1.2", "redux": "^4.2.1", "rxjs": "^7.8.0", @@ -89,6 +92,7 @@ "@types/react-dom": "^18.0.6", "@types/react-katex": "^3.0.0", "@types/react-transition-group": "^4.4.6", + "@types/react-window": "^1.8.8", "@types/utf8": "^3.0.1", "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.51.0", diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml index 2829d88eeb..695c178cde 100644 --- a/frontend/appflowy_tauri/pnpm-lock.yaml +++ b/frontend/appflowy_tauri/pnpm-lock.yaml @@ -130,6 +130,15 @@ dependencies: react-transition-group: specifier: ^4.4.5 version: 4.4.5(react-dom@18.2.0)(react@18.2.0) + react-virtualized-auto-sizer: + specifier: ^1.0.20 + version: 1.0.20(react-dom@18.2.0)(react@18.2.0) + react-vtree: + specifier: ^2.0.4 + version: 2.0.4(@types/react-window@1.8.8)(react-dom@18.2.0)(react-window@1.8.10)(react@18.2.0) + react-window: + specifier: ^1.8.10 + version: 1.8.10(react-dom@18.2.0)(react@18.2.0) react18-input-otp: specifier: ^1.1.2 version: 1.1.3(react-dom@18.2.0)(react@18.2.0) @@ -210,6 +219,9 @@ devDependencies: '@types/react-transition-group': specifier: ^4.4.6 version: 4.4.6 + '@types/react-window': + specifier: ^1.8.8 + version: 1.8.8 '@types/utf8': specifier: ^3.0.1 version: 3.0.1 @@ -2308,6 +2320,11 @@ packages: '@types/react': 18.2.6 dev: false + /@types/react-window@1.8.8: + resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} + dependencies: + '@types/react': 18.2.6 + /@types/react@17.0.59: resolution: {integrity: sha512-gSON5zWYIGyoBcycCE75E9+r6dCC2dHdsrVkOEiIYNU5+Q28HcBAuqvDuxHcCbMfHBHdeT5Tva/AFn3rnMKE4g==} dependencies: @@ -2733,7 +2750,7 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.22.10 + '@babel/runtime': 7.23.4 cosmiconfig: 7.1.0 resolve: 1.22.2 dev: false @@ -3235,7 +3252,7 @@ packages: /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.23.4 csstype: 3.1.2 dev: false @@ -5632,7 +5649,7 @@ packages: peerDependencies: react: ^16.3.0 dependencies: - '@babel/runtime': 7.22.10 + '@babel/runtime': 7.23.4 prop-types: 15.8.1 react: 18.2.0 warning: 4.0.3 @@ -5717,7 +5734,7 @@ packages: react-native: optional: true dependencies: - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.23.4 '@types/react-redux': 7.1.25 hoist-non-react-statics: 3.3.2 loose-envify: 1.4.0 @@ -5839,6 +5856,44 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-virtualized-auto-sizer@1.0.20(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA==} + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-vtree@2.0.4(@types/react-window@1.8.8)(react-dom@18.2.0)(react-window@1.8.10)(react@18.2.0): + resolution: {integrity: sha512-UOld0VqyAZrryF06K753X4bcEVN6/wW831exvVlMZeZAVHk9KXnlHs4rpqDAeoiBgUwJqoW/rtn0hwsokRRxPA==} + peerDependencies: + '@types/react-window': ^1.8.2 + react: ^16.13.1 + react-dom: ^16.13.1 + react-window: ^1.8.5 + dependencies: + '@babel/runtime': 7.23.4 + '@types/react-window': 1.8.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-window: 1.8.10(react-dom@18.2.0)(react@18.2.0) + dev: false + + /react-window@1.8.10(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.23.4 + memoize-one: 5.2.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react18-input-otp@1.1.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-55dZMVX61In2ngUhA4Fv0NMY4j5RZjxrJaSOAnJGJmkAhxKB6puVHYEmipyy2+W2CPydFF7pv+0NKzPUA03EVg==} peerDependencies: diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts index d742715fa3..765045b605 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts @@ -1,4 +1,4 @@ -import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { createContext, useCallback, useContext, useEffect, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { proxy, useSnapshot } from 'valtio'; @@ -52,6 +52,26 @@ export const DatabaseProvider = DatabaseContext.Provider; export const useDatabase = () => useSnapshot(useContext(DatabaseContext)); +export const useContextDatabase = () => useContext(DatabaseContext); + +export const useGetPrevRowId = () => { + const database = useContextDatabase(); + + return useCallback( + (id: string) => { + const rowMetas = database.rowMetas; + const index = rowMetas.findIndex((rowMeta) => rowMeta.id === id); + + if (index === 0) { + return null; + } + + return rowMetas[index - 1].id; + }, + [database] + ); +}; + export const useSelectorCell = (rowId: string, fieldId: string) => { const database = useContext(DatabaseContext); const cells = useSnapshot(database.cells); @@ -181,50 +201,3 @@ export const useConnectDatabase = (viewId: string) => { return database; }; - -export function useDatabaseResize(selectedViewId?: string) { - const ref = useRef(null); - const collectionRef = useRef(null); - const [openCollections, setOpenCollections] = useState([]); - - const [tableHeight, setTableHeight] = useState(0); - - useEffect(() => { - const element = ref.current; - - if (!element) return; - - const collectionElement = collectionRef.current; - const handleResize = () => { - const rect = element.getBoundingClientRect(); - const collectionRect = collectionElement?.getBoundingClientRect(); - let height = rect.height - 31; - - if (collectionRect) { - height -= collectionRect.height; - } - - setTableHeight(height); - }; - - handleResize(); - const resizeObserver = new ResizeObserver(handleResize); - - resizeObserver.observe(element); - if (collectionElement) { - resizeObserver.observe(collectionElement); - } - - return () => { - resizeObserver.disconnect(); - }; - }, [selectedViewId]); - - return { - ref, - collectionRef, - tableHeight, - openCollections, - setOpenCollections, - }; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx index f827bb62b9..416277c76f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useViewId } from '$app/hooks/ViewId.hooks'; import { databaseViewService } from './application'; import { DatabaseTabBar } from './components'; @@ -8,11 +8,11 @@ import { DatabaseCollection } from './components/database_settings'; import { PageController } from '$app/stores/effects/workspace/page/page_controller'; import SwipeableViews from 'react-swipeable-views'; import { TabPanel } from '$app/components/database/components/tab_bar/ViewTabs'; -import { useDatabaseResize } from '$app/components/database/Database.hooks'; import DatabaseSettings from '$app/components/database/components/database_settings/DatabaseSettings'; import { Portal } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { ErrorCode } from '@/services/backend'; +import ExpandRecordModal from '$app/components/database/components/edit_record/ExpandRecordModal'; interface Props { selectedViewId?: string; @@ -20,11 +20,13 @@ interface Props { } export const Database = ({ selectedViewId, setSelectedViewId }: Props) => { + const ref = useRef(null); const viewId = useViewId(); const { t } = useTranslation(); const [notFound, setNotFound] = useState(false); const [childViewIds, setChildViewIds] = useState([]); - const { ref, collectionRef, tableHeight, openCollections, setOpenCollections } = useDatabaseResize(selectedViewId); + const [editRecordRowId, setEditRecordRowId] = useState(null); + const [openCollections, setOpenCollections] = useState([]); useEffect(() => { const onPageChanged = () => { @@ -79,6 +81,13 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => { [openCollections, setOpenCollections] ); + const onEditRecord = useCallback( + (rowId: string) => { + setEditRecordRowId(rowId); + }, + [setEditRecordRowId] + ); + if (notFound) { return (
@@ -104,7 +113,7 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => { index={value} > {childViewIds.map((id, index) => ( - + {selectedViewId === id && ( <> @@ -115,13 +124,20 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => { />
-
- -
+ + {editRecordRowId && ( + { + setEditRecordRowId(null); + }} + /> + )} )} - + ))} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseView.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseView.tsx index 266ad6a100..98b16d5fea 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseView.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseView.tsx @@ -6,8 +6,7 @@ import { Board } from './board'; import { Calendar } from './calendar'; export const DatabaseView: FC<{ - tableHeight: number; - isActivated: boolean; + onEditRecord: (rowId: string) => void; }> = (props) => { const { layoutType } = useDatabase(); 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 index f9d03c2838..01666121cd 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/CellText.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/CellText.tsx @@ -9,8 +9,8 @@ export const CellText = React.forwardRef - {children} +
+ {children}
); } 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 index 83cdd4abdc..ce8afe6f31 100644 --- 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 @@ -10,6 +10,7 @@ export interface UseDraggableOptions { disabled?: boolean; scrollOnEdge?: { direction?: ScrollDirection; + getScrollElement?: () => HTMLElement | null; edgeGap?: number | Partial; }; } @@ -73,7 +74,8 @@ export const useDraggable = ({ return; } - const scrollParent: HTMLElement | null = getScrollParent(event.target as HTMLElement, scrollDirection); + const scrollParent: HTMLElement | null = + scrollOnEdge?.getScrollElement?.() ?? getScrollParent(event.target as HTMLElement, scrollDirection); if (scrollParent) { autoScrollOnEdge({ @@ -83,7 +85,7 @@ export const useDraggable = ({ }); } }, - [context, effectAllowed, scrollDirection, edgeGap] + [context, effectAllowed, scrollDirection, scrollOnEdge, edgeGap] ); const onDragEnd = useCallback(() => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_service.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_service.ts index 53bc56a6ec..9739c14b7d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_service.ts @@ -11,6 +11,7 @@ import { FieldSettingsChangesetPB, FieldVisibility, DatabaseViewIdPB, + CreateFieldPosition, } from '@/services/backend'; import { DatabaseEventDuplicateField, @@ -80,11 +81,25 @@ export async function getFields( return { fields, typeOptions }; } -export async function createField(viewId: string, fieldType?: FieldType, data?: Uint8Array): Promise { +export async function createField({ + viewId, + targetFieldId, + fieldPosition, + fieldType, + data, +}: { + viewId: string; + targetFieldId?: string; + fieldPosition?: CreateFieldPosition; + fieldType?: FieldType; + data?: Uint8Array; +}): Promise { const payload = CreateFieldPayloadPB.fromObject({ view_id: viewId, field_type: fieldType, type_option_data: data, + target_field_id: targetFieldId, + field_position: fieldPosition, }); const result = await DatabaseEventCreateField(payload); @@ -188,3 +203,12 @@ export async function updateFieldSetting( return result.val; } + +export const reorderFields = (list: Field[], startIndex: number, endIndex: number) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + + result.splice(endIndex, 0, removed); + + return result; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.hooks.ts index e234bf00ca..74be8bea8c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.hooks.ts @@ -29,7 +29,7 @@ export const useCell = (rowId: string, field: Field) => { clearTimeout(timeout); }; } - }, [fetchCell, cell, loading]); + }, [fetchCell, cell, loading, rowId, field.id]); useNotification(DatabaseNotification.DidUpdateCell, fetchCell, { id: `${rowId}:${field.id}` }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.tsx index c76e1e80cb..67175131e1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import React, { FC, HTMLAttributes } from 'react'; import { FieldType } from '@/services/backend'; import { Cell as CellType, Field } from '../../application'; @@ -12,18 +12,17 @@ import ChecklistCell from '$app/components/database/components/cell/ChecklistCel import DateTimeCell from '$app/components/database/components/cell/DateTimeCell'; import TimestampCell from '$app/components/database/components/cell/TimestampCell'; -export interface CellProps { +export interface CellProps extends HTMLAttributes { rowId: string; field: Field; - documentId?: string; icon?: string; placeholder?: string; } -interface CellComponentProps { - field: Field; +export interface CellComponentProps extends CellProps { cell: CellType; } + const getCellComponent = (fieldType: FieldType) => { switch (fieldType) { case FieldType.RichText: @@ -62,5 +61,5 @@ export const Cell: FC = ({ rowId, field, ...props }) => { return null; } - return ; + return ; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/CheckboxCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/CheckboxCell.tsx index d795d94669..cff353e31a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/CheckboxCell.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/CheckboxCell.tsx @@ -16,7 +16,10 @@ export const CheckboxCell: FC<{ }, [viewId, cell, field.id, checked]); return ( -
+
{checked ? : }
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/ExpandButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/ExpandButton.tsx deleted file mode 100644 index efb3b8c131..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/ExpandButton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { useState } from 'react'; -import { TextCell } from '$app/components/database/application'; -import { IconButton } from '@mui/material'; -import { ReactComponent as OpenIcon } from '$app/assets/open.svg'; - -const ExpandCellModal = React.lazy(() => import('$app/components/database/components/cell/expand_type/ExpandCellModal')); - -interface Props { - cell: TextCell; - visible?: boolean; - documentId?: string; - icon?: string; -} -function ExpandButton({ cell, documentId, icon, visible }: Props) { - const [open, setOpen] = useState(false); - - const onClose = () => { - setOpen(false); - }; - - return ( - <> - {visible && ( -
- setOpen(true)} className={'h-6 w-6 text-sm'}> - - -
- )} - - {open && documentId && ( - - )} - - ); -} - -export default ExpandButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/NumberCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/NumberCell.tsx index 859537481e..af13b65d07 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/NumberCell.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/NumberCell.tsx @@ -29,7 +29,7 @@ function NumberCell({ field, cell, placeholder }: Props) { return ( <> -
{content}
+
{content}
{editing && ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx index ccf3bfc430..66fae795af 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx @@ -1,28 +1,17 @@ -import { FC, FormEventHandler, Suspense, lazy, useCallback, useEffect, useRef, useMemo } from 'react'; -import { Field, TextCell as TextCellType } from '../../application'; +import { FC, FormEventHandler, Suspense, lazy, useCallback, useRef, useMemo } from 'react'; +import { TextCell as TextCellType } from '../../application'; import { CellText } from '../../_shared'; -import { useGridUIStateDispatcher, useGridUIStateSelector } from '$app/components/database/proxy/grid/ui_state/actions'; import { useInputCell } from '$app/components/database/components/cell/Cell.hooks'; -const ExpandButton = lazy(() => import('$app/components/database/components/cell/ExpandButton')); const EditTextCellInput = lazy(() => import('$app/components/database/components/field_types/text/EditTextCellInput')); -export const TextCell: FC<{ - field: Field; +interface TextCellProps { cell: TextCellType; - documentId?: string; - icon?: string; placeholder?: string; -}> = ({ field, documentId, icon, placeholder, cell }) => { - const isPrimary = field.isPrimary; +} +export const TextCell: FC = ({ placeholder, cell }) => { const cellRef = useRef(null); const { value, editing, updateCell, setEditing, setValue } = useInputCell(cell); - - const { hoverRowId } = useGridUIStateSelector(); - const isHover = hoverRowId === cell?.rowId; - const { setRowHover } = useGridUIStateDispatcher(); - - const showExpandIcon = cell && !editing && isHover && isPrimary; const handleClose = () => { if (!cell) return; updateCell(); @@ -41,12 +30,6 @@ export const TextCell: FC<{ [setValue] ); - useEffect(() => { - if (editing) { - setRowHover(null); - } - }, [editing, setRowHover]); - const content = useMemo(() => { if (cell && typeof cell.data === 'string' && cell.data) { return cell.data; @@ -56,33 +39,21 @@ export const TextCell: FC<{ }, [cell, placeholder]); return ( -
- -
- {icon &&
{icon}
} - {content} -
+ <> + + {content} - {cell && } {editing && ( )} -
+ ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/URLCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/URLCell.tsx index 5ace914589..d1c2c90db0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/URLCell.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/URLCell.tsx @@ -71,14 +71,13 @@ function UrlCell({ field, cell, placeholder }: Props) { ref={cellRef} onClick={handleClick} > -
{content}
+ {content} {editing && ( void; } function Properties({ onItemClick }: PropertiesProps) { const { fields } = useDatabase(); + const [state, setState] = useState(fields as FieldType[]); + const viewId = useViewId(); + + useEffect(() => { + setState(fields as FieldType[]); + }, [fields]); + + const handleOnDragEnd = async (result: DropResult) => { + const { destination, draggableId, source } = result; + const newIndex = destination?.index; + const oldIndex = source.index; + + if (oldIndex === newIndex) { + return; + } + + if (newIndex === undefined || newIndex === null) { + return; + } + + const newProperties = fieldService.reorderFields(fields as FieldType[], oldIndex, newIndex ?? 0); + + setState(newProperties); + + await fieldService.moveField(viewId, draggableId, oldIndex, newIndex); + }; return ( -
- {fields.map((field) => ( - onItemClick(field)} - className={'flex w-full items-center justify-between overflow-hidden px-1.5'} - key={field.id} - > -
- -
+ + + {(dropProvided) => ( +
+ {state.map((field, index) => ( + + {(provided) => { + return ( + + + + +
+ +
-
{field.visibility !== FieldVisibility.AlwaysHidden ? : }
-
- ))} -
+ onItemClick(field)} + className={'ml-2'} + > + {field.visibility !== FieldVisibility.AlwaysHidden ? : } + +
+ ); + }} + + ))} + {dropProvided.placeholder} +
+ )} + + ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/EditRecord.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/EditRecord.tsx index fcdcf5c915..40d403873f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/EditRecord.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/EditRecord.tsx @@ -1,19 +1,23 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { TextCell } from '$app/components/database/application'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import RecordDocument from '$app/components/database/components/edit_record/RecordDocument'; import RecordHeader from '$app/components/database/components/edit_record/RecordHeader'; import { Page } from '$app_reducers/pages/slice'; import { PageController } from '$app/stores/effects/workspace/page/page_controller'; import { ErrorCode, ViewLayoutPB } from '@/services/backend'; import { Log } from '$app/utils/log'; +import { useDatabase } from '$app/components/database'; interface Props { - cell: TextCell; - documentId: string; - icon?: string; + rowId: string; } -function EditRecord({ documentId: id, cell, icon }: Props) { + +function EditRecord({ rowId }: Props) { + const { rowMetas } = useDatabase(); + const row = useMemo(() => { + return rowMetas.find((row) => row.id === rowId); + }, [rowMetas, rowId]); const [page, setPage] = useState(null); + const id = row?.documentId; const loadPage = useCallback(async () => { if (!id) return; @@ -47,8 +51,10 @@ function EditRecord({ documentId: id, cell, icon }: Props) { }, [loadPage]); const getDocumentTitle = useCallback(() => { - return ; - }, [cell, icon, page]); + return row ? : null; + }, [row, page]); + + if (!id) return null; return (
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/expand_type/ExpandCellModal.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/ExpandRecordModal.tsx similarity index 76% rename from frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/expand_type/ExpandCellModal.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/ExpandRecordModal.tsx index 5a266847ba..1367e042d3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/expand_type/ExpandCellModal.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/ExpandRecordModal.tsx @@ -1,23 +1,20 @@ import React, { useState } from 'react'; -import { DialogProps, IconButton } from '@mui/material'; +import { DialogProps, IconButton, Portal } from '@mui/material'; import DialogContent from '@mui/material/DialogContent'; import Dialog from '@mui/material/Dialog'; -import { TextCell } from '$app/components/database/application'; import { ReactComponent as DetailsIcon } from '$app/assets/details.svg'; import RecordActions from '$app/components/database/components/edit_record/RecordActions'; import EditRecord from '$app/components/database/components/edit_record/EditRecord'; interface Props extends DialogProps { - cell: TextCell; - documentId: string; - icon?: string; + rowId: string; } -function ExpandCellModal({ open, onClose, cell, documentId, icon }: Props) { +function ExpandRecordModal({ open, onClose, rowId }: Props) { const [detailAnchorEl, setDetailAnchorEl] = useState(null); return ( - <> + - + setDetailAnchorEl(null)} /> - + ); } -export default ExpandCellModal; +export default ExpandRecordModal; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordActions.tsx index 9fbbdaa6e2..e70b3be5d1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordActions.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordActions.tsx @@ -3,17 +3,16 @@ import { Icon, Menu, MenuProps } from '@mui/material'; import { ReactComponent as DelSvg } from '$app/assets/delete.svg'; import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; import { useTranslation } from 'react-i18next'; -import { Cell, rowService } from '$app/components/database/application'; +import { rowService } from '$app/components/database/application'; import { useViewId } from '$app/hooks'; import MenuItem from '@mui/material/MenuItem'; interface Props extends MenuProps { - cell: Cell; + rowId: string; onClose?: () => void; } -function RecordActions({ anchorEl, open, onClose, cell }: Props) { +function RecordActions({ anchorEl, open, onClose, rowId }: Props) { const viewId = useViewId(); - const rowId = cell.rowId; const { t } = useTranslation(); const handleDelRow = useCallback(() => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordHeader.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordHeader.tsx index 87b304f45c..01473d5b1f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordHeader.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordHeader.tsx @@ -2,15 +2,14 @@ import React, { useEffect, useRef } from 'react'; import RecordTitle from '$app/components/database/components/edit_record/RecordTitle'; import RecordProperties from '$app/components/database/components/edit_record/record_properties/RecordProperties'; import { Divider } from '@mui/material'; -import { TextCell } from '$app/components/database/application'; +import { RowMeta } from '$app/components/database/application'; import { Page } from '$app_reducers/pages/slice'; interface Props { page: Page | null; - cell: TextCell; - icon?: string; + row: RowMeta; } -function RecordHeader({ page, cell, icon }: Props) { +function RecordHeader({ page, row }: Props) { const ref = useRef(null); useEffect(() => { @@ -30,8 +29,8 @@ function RecordHeader({ page, cell, icon }: Props) { return (
- - + +
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordTitle.tsx index 679f2ecb88..8908b0de5e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordTitle.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordTitle.tsx @@ -1,30 +1,38 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { Page, PageIcon } from '$app_reducers/pages/slice'; import ViewTitle from '$app/components/_shared/ViewTitle'; import { ViewIconTypePB } from '@/services/backend'; import { useViewId } from '$app/hooks'; import { updateRowMeta } from '$app/components/database/application/row/row_service'; -import { cellService, TextCell } from '$app/components/database/application'; +import { cellService, Field, RowMeta, TextCell } from '$app/components/database/application'; +import { useDatabase } from '$app/components/database'; +import { useCell } from '$app/components/database/components/cell/Cell.hooks'; interface Props { page: Page | null; - icon?: string; - cell: TextCell; + row: RowMeta; } -function RecordTitle({ cell, page, icon }: Props) { - const { data: title, fieldId, rowId } = cell; +function RecordTitle({ row, page }: Props) { + const { fields } = useDatabase(); + const field = useMemo(() => { + return fields.find((field) => field.isPrimary) as Field; + }, [fields]); + const rowId = row.id; + const cell = useCell(rowId, field) as TextCell; + const title = cell.data; + const viewId = useViewId(); const onTitleChange = useCallback( async (title: string) => { try { - await cellService.updateCell(viewId, rowId, fieldId, title); + await cellService.updateCell(viewId, rowId, field.id, title); } catch (e) { // toast.error('Failed to update title'); } }, - [fieldId, rowId, viewId] + [field.id, rowId, viewId] ); const onUpdateIcon = useCallback( @@ -47,10 +55,10 @@ function RecordTitle({ cell, page, icon }: Props) { view={{ ...page, name: title, - icon: icon + icon: row.icon ? { ty: ViewIconTypePB.Emoji, - value: icon, + value: row.icon, } : undefined, }} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/NewProperty.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/NewProperty.tsx deleted file mode 100644 index ece3405104..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/NewProperty.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { MouseEvent, useCallback, useMemo, useState } from 'react'; -import { fieldService } from '$app/components/database/application'; -import { FieldType } from '@/services/backend'; -import { useTranslation } from 'react-i18next'; -import Button from '@mui/material/Button'; -import { ReactComponent as AddSvg } from '$app/assets/add.svg'; -import { useViewId } from '$app/hooks'; -import { FieldMenu } from '$app/components/database/components/field/FieldMenu'; -import { useDatabase } from '$app/components/database'; - -function NewProperty() { - const viewId = useViewId(); - const { t } = useTranslation(); - const [anchorEl, setAnchorEl] = useState(null); - const open = Boolean(anchorEl); - const [updateFieldId, setUpdateFieldId] = useState(''); - const { fields } = useDatabase(); - const updateField = useMemo(() => fields.find((field) => field.id === updateFieldId), [fields, updateFieldId]); - - const handleClick = useCallback( - async (e: MouseEvent) => { - try { - const field = await fieldService.createField(viewId, FieldType.RichText); - - setUpdateFieldId(field.id); - setAnchorEl(e.target as HTMLButtonElement); - } catch (e) { - // toast.error(t('grid.field.newPropertyFail')); - } - }, - [viewId] - ); - - return ( - <> - - {updateField && ( - { - setUpdateFieldId(''); - setAnchorEl(null); - }} - /> - )} - - ); -} - -export default NewProperty; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/Property.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/Property.tsx index c267f5f84d..023194e811 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/Property.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/Property.tsx @@ -1,26 +1,26 @@ -import React, { HTMLAttributes, useCallback, useState } from 'react'; +import React, { HTMLAttributes } from 'react'; import PropertyName from '$app/components/database/components/edit_record/record_properties/PropertyName'; import PropertyValue from '$app/components/database/components/edit_record/record_properties/PropertyValue'; import { Field } from '$app/components/database/application'; -import PropertyActions from '$app/components/database/components/edit_record/record_properties/PropertyActions'; +import { IconButton, Tooltip } from '@mui/material'; +import { ReactComponent as DragSvg } from '$app/assets/drag.svg'; +import { useTranslation } from 'react-i18next'; interface Props extends HTMLAttributes { field: Field; rowId: string; ishovered: boolean; onHover: (id: string | null) => void; + menuOpened?: boolean; + onOpenMenu?: () => void; + onCloseMenu?: () => void; } -function Property({ field, rowId, ishovered, onHover, ...props }: Props, ref: React.ForwardedRef) { - const [openMenu, setOpenMenu] = useState(false); - - const handleOpenMenu = useCallback(() => { - setOpenMenu(true); - }, []); - - const handleCloseMenu = useCallback(() => { - setOpenMenu(false); - }, []); +function Property( + { field, rowId, ishovered, onHover, menuOpened, onCloseMenu, onOpenMenu, ...props }: Props, + ref: React.ForwardedRef +) { + const { t } = useTranslation(); return ( <> @@ -36,12 +36,20 @@ function Property({ field, rowId, ishovered, onHover, ...props }: Props, ref: Re key={field.id} {...props} > - + - {ishovered && } + {ishovered && ( +
+ + + + + +
+ )}
); } -export default React.memo(React.forwardRef(Property)); +export default React.forwardRef(Property); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyActions.tsx deleted file mode 100644 index 26abe1cc41..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyActions.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React, { forwardRef } from 'react'; -import { t } from 'i18next'; -import { IconButton, Tooltip } from '@mui/material'; -import { ReactComponent as DragSvg } from '$app/assets/drag.svg'; - -interface Props { - onOpenMenu: () => void; -} - -export default forwardRef(function PropertyActions({ onOpenMenu }, ref) { - return ( -
- - - - - -
- ); -}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyList.tsx index 4d271f8998..aa93b25179 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyList.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyList.tsx @@ -8,10 +8,12 @@ interface Props extends HTMLAttributes { properties: Field[]; rowId: string; placeholderNode?: React.ReactNode; + openMenuPropertyId?: string; + setOpenMenuPropertyId?: (id?: string) => void; } function PropertyList( - { documentId, properties, rowId, placeholderNode, ...props }: Props, + { documentId, properties, rowId, placeholderNode, openMenuPropertyId, setOpenMenuPropertyId, ...props }: Props, ref: React.ForwardedRef ) { const [hoverId, setHoverId] = useState(null); @@ -44,6 +46,15 @@ function PropertyList( ishovered={field.id === hoverId} field={field} rowId={rowId} + menuOpened={openMenuPropertyId === field.id} + onOpenMenu={() => { + setOpenMenuPropertyId?.(field.id); + }} + onCloseMenu={() => { + if (openMenuPropertyId === field.id) { + setOpenMenuPropertyId?.(undefined); + } + }} /> ); }} @@ -55,4 +66,4 @@ function PropertyList( ); } -export default React.memo(React.forwardRef(PropertyList)); +export default React.forwardRef(PropertyList); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyName.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyName.tsx index 13067279a3..b2c2bb16de 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyName.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyName.tsx @@ -1,15 +1,14 @@ import React, { useRef } from 'react'; -import { Field } from '$app/components/database/components/field'; +import { Property } from '$app/components/database/components/property'; import { Field as FieldType } from '$app/components/database/application'; -import { FieldMenu } from '$app/components/database/components/field/FieldMenu'; interface Props { field: FieldType; - openMenu: boolean; - onOpenMenu: () => void; - onCloseMenu: () => void; + menuOpened?: boolean; + onOpenMenu?: () => void; + onCloseMenu?: () => void; } -function PropertyName({ field, openMenu, onOpenMenu, onCloseMenu }: Props) { +function PropertyName({ field, menuOpened = false, onOpenMenu, onCloseMenu }: Props) { const ref = useRef(null); return ( @@ -19,14 +18,13 @@ function PropertyName({ field, openMenu, onOpenMenu, onCloseMenu }: Props) { onContextMenu={(e) => { e.stopPropagation(); e.preventDefault(); - onOpenMenu(); + onOpenMenu?.(); }} className={'flex min-h-[36px] w-[200px] cursor-pointer items-center'} onClick={onOpenMenu} > - +
- {openMenu && } ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/RecordProperties.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/RecordProperties.tsx index 00b990de6b..d6f65edced 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/RecordProperties.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/RecordProperties.tsx @@ -1,38 +1,27 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Field, fieldService, TextCell } from '$app/components/database/application'; +import { Field, fieldService, RowMeta } from '$app/components/database/application'; import { useDatabase } from '$app/components/database'; import { FieldVisibility } from '@/services/backend'; -import Button from '@mui/material/Button'; - -import { ReactComponent as EyeClosedSvg } from '$app/assets/eye_close.svg'; -import { ReactComponent as EyeOpenSvg } from '$app/assets/eye_open.svg'; import PropertyList from '$app/components/database/components/edit_record/record_properties/PropertyList'; -import NewProperty from '$app/components/database/components/edit_record/record_properties/NewProperty'; +import NewProperty from '$app/components/database/components/property/NewProperty'; import { useViewId } from '$app/hooks'; import { DragDropContext, Droppable, DropResult, OnDragEndResponder } from 'react-beautiful-dnd'; -import { useTranslation } from 'react-i18next'; +import SwitchPropertiesVisible from '$app/components/database/components/edit_record/record_properties/SwitchPropertiesVisible'; interface Props { documentId?: string; - cell: TextCell; + row: RowMeta; } -// a little function to help us with reordering the result -const reorder = (list: Field[], startIndex: number, endIndex: number) => { - const result = Array.from(list); - const [removed] = result.splice(startIndex, 1); - - result.splice(endIndex, 0, removed); - - return result; -}; - -function RecordProperties({ documentId, cell }: Props) { - const { t } = useTranslation(); +function RecordProperties({ documentId, row }: Props) { const viewId = useViewId(); - const { fieldId, rowId } = cell; const { fields } = useDatabase(); + const fieldId = useMemo(() => { + return fields.find((field) => field.isPrimary)?.id; + }, [fields]); + const rowId = row.id; + const [openMenuPropertyId, setOpenMenuPropertyId] = useState(undefined); const [showHiddenFields, setShowHiddenFields] = useState(false); const properties = useMemo(() => { @@ -84,7 +73,7 @@ function RecordProperties({ documentId, cell }: Props) { } // reorder the properties synchronously to avoid flickering - const newProperties = reorder(properties, oldIndex, newIndex ?? 0); + const newProperties = fieldService.reorderFields(properties, oldIndex, newIndex ?? 0); setState(newProperties); @@ -111,33 +100,19 @@ function RecordProperties({ documentId, cell }: Props) { {...dropProvided.droppableProps} rowId={rowId} properties={state} + openMenuPropertyId={openMenuPropertyId} + setOpenMenuPropertyId={setOpenMenuPropertyId} /> )} - { - // show the button only if there are hidden fields - hiddenFieldsCount > 0 && ( - - ) - } + - + ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/SwitchPropertiesVisible.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/SwitchPropertiesVisible.tsx new file mode 100644 index 0000000000..f8937bbf21 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/SwitchPropertiesVisible.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as EyeClosedSvg } from '$app/assets/eye_close.svg'; +import { ReactComponent as EyeOpenSvg } from '$app/assets/eye_open.svg'; + +function SwitchPropertiesVisible({ + hiddenFieldsCount, + showHiddenFields, + setShowHiddenFields, +}: { + hiddenFieldsCount: number; + showHiddenFields: boolean; + setShowHiddenFields: (showHiddenFields: boolean) => void; +}) { + const { t } = useTranslation(); + + return hiddenFieldsCount > 0 ? ( + + ) : null; +} + +export default SwitchPropertiesVisible; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/Field.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/Field.tsx deleted file mode 100644 index 2d77408cb9..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/Field.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { FC } from 'react'; -import { Field as FieldType } from '../../application'; -import { FieldTypeSvg } from './FieldTypeSvg'; - -export interface FieldProps { - field: FieldType; -} - -export const Field: FC = ({ field }) => { - return ( -
- - {field.name} -
- ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldMenu.tsx deleted file mode 100644 index c35b0908fc..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldMenu.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Divider, MenuList, MenuProps } from '@mui/material'; -import { ChangeEventHandler, FC, useCallback, useState } from 'react'; -import { useViewId } from '$app/hooks'; -import { Field, fieldService } from '../../application'; -import { FieldMenuActions } from './FieldMenuActions'; -import FieldTypeMenuExtension from '$app/components/database/components/field/FieldTypeMenuExtension'; -import FieldTypeSelect from '$app/components/database/components/field/FieldTypeSelect'; -import { FieldType } from '@/services/backend'; -import { Log } from '$app/utils/log'; -import TextField from '@mui/material/TextField'; -import Popover from '@mui/material/Popover'; - -export interface GridFieldMenuProps { - field: Field; - anchorEl: MenuProps['anchorEl']; - open: boolean; - onClose: () => void; -} - -export const FieldMenu: FC = ({ field, anchorEl, open, onClose }) => { - const viewId = useViewId(); - const [inputtingName, setInputtingName] = useState(field.name); - - const handleInput = useCallback>((e) => { - setInputtingName(e.target.value); - }, []); - - const handleBlur = useCallback(async () => { - if (inputtingName !== field.name) { - try { - await fieldService.updateField(viewId, field.id, { - name: inputtingName, - }); - } catch (e) { - // TODO - Log.error(`change field ${field.id} name from '${field.name}' to ${inputtingName} fail`, e); - } - } - }, [viewId, field, inputtingName]); - - const isPrimary = field.isPrimary; - - const onUpdateFieldType = useCallback( - async (type: FieldType) => { - try { - await fieldService.updateFieldType(viewId, field.id, type); - } catch (e) { - // TODO - Log.error(`change field ${field.id} type from '${field.type}' to ${type} fail`, e); - } - }, - [viewId, field] - ); - - return ( - - - -
- {!isPrimary && ( - <> - - - - )} - - onClose()} fieldId={field.id} /> -
-
-
- ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldMenuActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldMenuActions.tsx deleted file mode 100644 index 8d5d5c41a3..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldMenuActions.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { Grid, MenuItem } from '@mui/material'; -import { t } from 'i18next'; - -import { ReactComponent as HideSvg } from '$app/assets/hide.svg'; -import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; -import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg'; -import { ReactComponent as LeftSvg } from '$app/assets/left.svg'; -import { ReactComponent as RightSvg } from '$app/assets/right.svg'; -import { fieldService } from '$app/components/database/application'; -import { FieldVisibility } from '@/services/backend'; -import { useViewId } from '$app/hooks'; -import ConfirmDialog from '$app/components/_shared/app-dialog/ConfirmDialog'; -import { useState } from 'react'; - -enum FieldAction { - Hide = 'hide', - Duplicate = 'duplicate', - Delete = 'delete', - InsertLeft = 'insertLeft', - InsertRight = 'insertRight', -} - -const FieldActionSvgMap = { - [FieldAction.Hide]: HideSvg, - [FieldAction.Duplicate]: CopySvg, - [FieldAction.Delete]: DeleteSvg, - [FieldAction.InsertLeft]: LeftSvg, - [FieldAction.InsertRight]: RightSvg, -}; - -const TwoColumnActions: FieldAction[][] = [ - [FieldAction.Hide, FieldAction.Duplicate, FieldAction.Delete], - // [FieldAction.InsertLeft, FieldAction.InsertRight], -]; - -// prevent default actions for primary fields -const primaryPreventDefaultActions = [FieldAction.Delete, FieldAction.Duplicate]; - -interface GridFieldMenuActionsProps { - fieldId: string; - isPrimary?: boolean; - onMenuItemClick?: (action: FieldAction) => void; -} - -export const FieldMenuActions = ({ fieldId, onMenuItemClick, isPrimary }: GridFieldMenuActionsProps) => { - const viewId = useViewId(); - const [openConfirm, setOpenConfirm] = useState(false); - - const handleOpenConfirm = () => { - setOpenConfirm(true); - }; - - const handleMenuItemClick = async (action: FieldAction) => { - const preventDefault = isPrimary && primaryPreventDefaultActions.includes(action); - - if (preventDefault) { - return; - } - - switch (action) { - case FieldAction.Hide: - await fieldService.updateFieldSetting(viewId, fieldId, { - visibility: FieldVisibility.AlwaysHidden, - }); - break; - case FieldAction.Duplicate: - await fieldService.duplicateField(viewId, fieldId); - break; - case FieldAction.Delete: - handleOpenConfirm(); - return; - } - - onMenuItemClick?.(action); - }; - - return ( - - {TwoColumnActions.map((column, index) => ( - - {column.map((action) => { - const ActionSvg = FieldActionSvgMap[action]; - const disabled = isPrimary && primaryPreventDefaultActions.includes(action); - - return ( - handleMenuItemClick(action)} key={action} dense> - - {t(`grid.field.${action}`)} - - ); - })} - - ))} - { - await fieldService.deleteField(viewId, fieldId); - }} - onClose={() => { - setOpenConfirm(false); - onMenuItemClick?.(FieldAction.Delete); - }} - /> - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/index.ts deleted file mode 100644 index 81a22762c6..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './Field'; -export * from './FieldSelect'; -export * from './FieldTypeText'; -export * from './FieldTypeSvg'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx index 4c79482120..28e8c93145 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx @@ -4,12 +4,11 @@ import { Popover, TextareaAutosize } from '@mui/material'; interface Props { editing: boolean; anchorEl: HTMLDivElement | null; - width: number | undefined; onClose: () => void; text: string; onInput: (event: React.FormEvent) => void; } -function EditTextCellInput({ editing, anchorEl, width, onClose, text, onInput }: Props) { +function EditTextCellInput({ editing, anchorEl, onClose, text, onInput }: Props) { const handleEnter = (e: React.KeyboardEvent) => { const shift = e.shiftKey; @@ -27,7 +26,7 @@ function EditTextCellInput({ editing, anchorEl, width, onClose, text, onInput }: anchorEl={anchorEl} PaperProps={{ className: 'flex p-2 border border-blue-400', - style: { width, minHeight: anchorEl?.offsetHeight, borderRadius: 0, boxShadow: 'none' }, + style: { width: anchorEl?.offsetWidth, minHeight: anchorEl?.offsetHeight, borderRadius: 0, boxShadow: 'none' }, }} transformOrigin={{ vertical: 1, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filter.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filter.tsx index 7e786186cb..13d751d5ce 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filter.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filter.tsx @@ -11,7 +11,7 @@ import { DateFilterData, } from '$app/components/database/application'; import { Chip, Popover } from '@mui/material'; -import { Field } from '$app/components/database/components/field'; +import { Property } from '$app/components/database/components/property'; import { ReactComponent as DropDownSvg } from '$app/assets/dropdown.svg'; import TextFilter from './text_filter/TextFilter'; import { FieldType } from '@/services/backend'; @@ -111,7 +111,7 @@ function Filter({ filter, field }: Props) { variant='outlined' label={
- +
} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterFieldsMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterFieldsMenu.tsx index ce7109776d..c435fc3bf8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterFieldsMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterFieldsMenu.tsx @@ -1,6 +1,6 @@ import React, { MouseEvent, useCallback } from 'react'; import { MenuProps } from '@mui/material'; -import FieldList from '$app/components/database/components/field/FieldList'; +import PropertiesList from '$app/components/database/components/property/PropertiesList'; import { Field } from '$app/components/database/application'; import { useViewId } from '$app/hooks'; import { useTranslation } from 'react-i18next'; @@ -35,7 +35,7 @@ function FilterFieldsMenu({ return ( - + ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/NewProperty.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/NewProperty.tsx new file mode 100644 index 0000000000..ca501ca231 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/NewProperty.tsx @@ -0,0 +1,36 @@ +import React, { useCallback } from 'react'; +import { fieldService } from '$app/components/database/application'; +import { FieldType } from '@/services/backend'; +import { useTranslation } from 'react-i18next'; +import Button from '@mui/material/Button'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { useViewId } from '$app/hooks'; + +interface NewPropertyProps { + onInserted?: (id: string) => void; +} +function NewProperty({ onInserted }: NewPropertyProps) { + const viewId = useViewId(); + const { t } = useTranslation(); + + const handleClick = useCallback(async () => { + try { + const field = await fieldService.createField({ + viewId, + fieldType: FieldType.RichText, + }); + + onInserted?.(field.id); + } catch (e) { + // toast.error(t('grid.field.newPropertyFail')); + } + }, [onInserted, viewId]); + + return ( + + ); +} + +export default NewProperty; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertiesList.tsx similarity index 88% rename from frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldList.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertiesList.tsx index 03e197c601..89a69141de 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldList.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertiesList.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import { OutlinedInput, MenuItem, MenuList } from '@mui/material'; -import { Field } from '$app/components/database/components/field/Field'; +import { Property } from '$app/components/database/components/property/Property'; import { Field as FieldType } from '../../application'; import { useDatabase } from '$app/components/database'; @@ -10,7 +10,7 @@ interface FieldListProps { onItemClick?: (event: React.MouseEvent, field: FieldType) => void; } -function FieldList({ showSearch, onItemClick, searchPlaceholder }: FieldListProps) { +function PropertiesList({ showSearch, onItemClick, searchPlaceholder }: FieldListProps) { const { fields } = useDatabase(); const [fieldsResult, setFieldsResult] = useState(fields as FieldType[]); @@ -52,7 +52,7 @@ function FieldList({ showSearch, onItemClick, searchPlaceholder }: FieldListProp onItemClick?.(event, field); }} > - + ))} @@ -60,4 +60,4 @@ function FieldList({ showSearch, onItemClick, searchPlaceholder }: FieldListProp ); } -export default FieldList; +export default PropertiesList; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/Property.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/Property.tsx new file mode 100644 index 0000000000..ea4de01e03 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/Property.tsx @@ -0,0 +1,61 @@ +import { FC, useEffect, useRef, useState } from 'react'; +import { Field as FieldType } from '../../application'; +import { ProppertyTypeSvg } from './property_type/ProppertyTypeSvg'; +import { PropertyMenu } from '$app/components/database/components/property/PropertyMenu'; + +export interface FieldProps { + field: FieldType; + menuOpened?: boolean; + onOpenMenu?: (id: string) => void; + onCloseMenu?: (id: string) => void; +} + +export const Property: FC = ({ field, onCloseMenu, menuOpened }) => { + const ref = useRef(null); + const [anchorPosition, setAnchorPosition] = useState< + | { + top: number; + left: number; + } + | undefined + >(undefined); + + const open = Boolean(anchorPosition) && menuOpened; + + useEffect(() => { + if (menuOpened) { + const rect = ref.current?.getBoundingClientRect(); + + if (rect) { + setAnchorPosition({ + top: rect.top + rect.height, + left: rect.left, + }); + return; + } + } + + setAnchorPosition(undefined); + }, [menuOpened]); + + return ( + <> +
+ + {field.name} +
+ + {open && ( + { + onCloseMenu?.(field.id); + }} + anchorPosition={anchorPosition} + anchorReference={'anchorPosition'} + /> + )} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyActions.tsx new file mode 100644 index 0000000000..b324e38b30 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyActions.tsx @@ -0,0 +1,143 @@ +import React, { useMemo, useState } from 'react'; + +import { ReactComponent as EditSvg } from '$app/assets/edit.svg'; +import { ReactComponent as HideSvg } from '$app/assets/hide.svg'; +import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; +import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg'; +import { ReactComponent as LeftSvg } from '$app/assets/left.svg'; +import { ReactComponent as RightSvg } from '$app/assets/right.svg'; +import { useViewId } from '$app/hooks'; +import { fieldService } from '$app/components/database/application'; +import { CreateFieldPosition, FieldVisibility } from '@/services/backend'; +import { MenuItem } from '@mui/material'; +import ConfirmDialog from '$app/components/_shared/app-dialog/ConfirmDialog'; +import { useTranslation } from 'react-i18next'; + +export enum FieldAction { + EditProperty, + Hide, + Duplicate, + Delete, + InsertLeft, + InsertRight, +} + +const FieldActionSvgMap = { + [FieldAction.EditProperty]: EditSvg, + [FieldAction.Hide]: HideSvg, + [FieldAction.Duplicate]: CopySvg, + [FieldAction.Delete]: DeleteSvg, + [FieldAction.InsertLeft]: LeftSvg, + [FieldAction.InsertRight]: RightSvg, +}; + +const defaultActions: FieldAction[] = [ + FieldAction.EditProperty, + FieldAction.InsertLeft, + FieldAction.InsertRight, + FieldAction.Hide, + FieldAction.Duplicate, + FieldAction.Delete, +]; + +// prevent default actions for primary fields +const primaryPreventDefaultActions = [FieldAction.Hide, FieldAction.Delete, FieldAction.Duplicate]; + +interface PropertyActionsProps { + fieldId: string; + actions?: FieldAction[]; + isPrimary?: boolean; + onMenuItemClick?: (action: FieldAction, newFieldId?: string) => void; +} + +function PropertyActions({ fieldId, onMenuItemClick, isPrimary, actions = defaultActions }: PropertyActionsProps) { + const viewId = useViewId(); + const { t } = useTranslation(); + const [openConfirm, setOpenConfirm] = useState(false); + + const menuTextMap = useMemo( + () => ({ + [FieldAction.EditProperty]: t('grid.field.editProperty'), + [FieldAction.Hide]: t('grid.field.hide'), + [FieldAction.Duplicate]: t('grid.field.duplicate'), + [FieldAction.Delete]: t('grid.field.delete'), + [FieldAction.InsertLeft]: t('grid.field.insertLeft'), + [FieldAction.InsertRight]: t('grid.field.insertRight'), + }), + [t] + ); + + const handleOpenConfirm = () => { + setOpenConfirm(true); + }; + + const handleMenuItemClick = async (action: FieldAction) => { + const preventDefault = isPrimary && primaryPreventDefaultActions.includes(action); + + if (preventDefault) { + return; + } + + switch (action) { + case FieldAction.EditProperty: + break; + case FieldAction.InsertLeft: + case FieldAction.InsertRight: { + const fieldPosition = action === FieldAction.InsertLeft ? CreateFieldPosition.Before : CreateFieldPosition.After; + + const field = await fieldService.createField({ + viewId, + fieldPosition, + targetFieldId: fieldId, + }); + + onMenuItemClick?.(action, field.id); + return; + } + + case FieldAction.Hide: + await fieldService.updateFieldSetting(viewId, fieldId, { + visibility: FieldVisibility.AlwaysHidden, + }); + break; + case FieldAction.Duplicate: + await fieldService.duplicateField(viewId, fieldId); + break; + case FieldAction.Delete: + handleOpenConfirm(); + return; + } + + onMenuItemClick?.(action); + }; + + return ( + <> + {actions.map((action) => { + const ActionSvg = FieldActionSvgMap[action]; + const disabled = isPrimary && primaryPreventDefaultActions.includes(action); + + return ( + handleMenuItemClick(action)} key={action} dense> + + {menuTextMap[action]} + + ); + })} + { + await fieldService.deleteField(viewId, fieldId); + }} + onClose={() => { + setOpenConfirm(false); + onMenuItemClick?.(FieldAction.Delete); + }} + /> + + ); +} + +export default PropertyActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyMenu.tsx new file mode 100644 index 0000000000..19579044ac --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyMenu.tsx @@ -0,0 +1,72 @@ +import { Divider, MenuList } from '@mui/material'; +import { FC, useCallback } from 'react'; +import { useViewId } from '$app/hooks'; +import { Field, fieldService } from '../../application'; +import PropertyTypeMenuExtension from '$app/components/database/components/property/property_type/PropertyTypeMenuExtension'; +import PropertyTypeSelect from '$app/components/database/components/property/property_type/PropertyTypeSelect'; +import { FieldType } from '@/services/backend'; +import { Log } from '$app/utils/log'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import PropertyNameInput from '$app/components/database/components/property/PropertyNameInput'; +import PropertyActions, { FieldAction } from '$app/components/database/components/property/PropertyActions'; + +const actions = [FieldAction.Hide, FieldAction.Duplicate, FieldAction.Delete]; + +export interface GridFieldMenuProps extends PopoverProps { + field: Field; +} + +export const PropertyMenu: FC = ({ field, ...props }) => { + const viewId = useViewId(); + + const isPrimary = field.isPrimary; + + const onUpdateFieldType = useCallback( + async (type: FieldType) => { + try { + await fieldService.updateFieldType(viewId, field.id, type); + } catch (e) { + // TODO + Log.error(`change field ${field.id} type from '${field.type}' to ${type} fail`, e); + } + }, + [viewId, field] + ); + + return ( + e.stopPropagation()} + keepMounted={false} + {...props} + > + + +
+ {!isPrimary && ( + <> + + + + )} + + { + props.onClose?.({}, 'backdropClick'); + }} + fieldId={field.id} + /> +
+
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyNameInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyNameInput.tsx new file mode 100644 index 0000000000..7d0d806fd7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyNameInput.tsx @@ -0,0 +1,47 @@ +import React, { ChangeEventHandler, useCallback, useState } from 'react'; +import { useViewId } from '$app/hooks'; +import { fieldService } from '$app/components/database/application'; +import { Log } from '$app/utils/log'; +import TextField from '@mui/material/TextField'; + +function PropertyNameInput({ id, name }: { id: string; name: string }) { + const viewId = useViewId(); + const [inputtingName, setInputtingName] = useState(name); + + const handleInput = useCallback>((e) => { + setInputtingName(e.target.value); + }, []); + + const handleSubmit = useCallback(async () => { + if (inputtingName !== name) { + try { + await fieldService.updateField(viewId, id, { + name: inputtingName, + }); + } catch (e) { + // TODO + Log.error(`change field ${id} name from '${name}' to ${inputtingName} fail`, e); + } + } + }, [viewId, id, name, inputtingName]); + + return ( + { + if (e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + void handleSubmit(); + } + }} + value={inputtingName} + onChange={handleInput} + onBlur={handleSubmit} + /> + ); +} + +export default PropertyNameInput; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertySelect.tsx similarity index 87% rename from frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldSelect.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertySelect.tsx index 1aa3022845..1a798d965b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldSelect.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertySelect.tsx @@ -2,13 +2,13 @@ import { MenuItem, Select, SelectChangeEvent, SelectProps } from '@mui/material' import { FC, useCallback } from 'react'; import { Field as FieldType } from '../../application'; import { useDatabase } from '../../Database.hooks'; -import { Field } from './Field'; +import { Property } from './Property'; export interface FieldSelectProps extends Omit { onChange?: (field: FieldType | undefined) => void; } -export const FieldSelect: FC = ({ onChange, ...props }) => { +export const PropertySelect: FC = ({ onChange, ...props }) => { const { fields } = useDatabase(); const handleChange = useCallback( @@ -36,7 +36,7 @@ export const FieldSelect: FC = ({ onChange, ...props }) => { > {fields.map((field) => ( - + ))} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/index.ts new file mode 100644 index 0000000000..0b338836d6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/index.ts @@ -0,0 +1,4 @@ +export * from './Property'; +export * from './PropertySelect'; +export * from './property_type/PropertyTypeText'; +export * from './property_type/ProppertyTypeSvg'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenu.tsx similarity index 86% rename from frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeMenu.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenu.tsx index 8d23196ebd..2d58bd9d8d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenu.tsx @@ -1,7 +1,7 @@ import { Divider, Menu, MenuItem, MenuProps } from '@mui/material'; import { FC, useMemo } from 'react'; import { FieldType } from '@/services/backend'; -import { FieldTypeText, FieldTypeSvg } from '$app/components/database/components/field/index'; +import { PropertyTypeText, ProppertyTypeSvg } from '$app/components/database/components/property'; import { Field } from '$app/components/database/application'; import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg'; @@ -25,7 +25,7 @@ const FieldTypeGroup = [ }, ]; -export const FieldTypeMenu: FC< +export const PropertyTypeMenu: FC< MenuProps & { field: Field; onClickItem?: (type: FieldType) => void; @@ -47,9 +47,9 @@ export const FieldTypeMenu: FC< , group.types.map((type) => ( onClickItem?.(type)} key={type} dense className={'flex justify-between'}> - + - + {type === field.type && } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeMenuExtension.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenuExtension.tsx similarity index 90% rename from frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeMenuExtension.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenuExtension.tsx index 84dac7724e..2a5c488194 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeMenuExtension.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenuExtension.tsx @@ -5,7 +5,7 @@ import SelectFieldActions from '$app/components/database/components/field_types/ import NumberFieldActions from '$app/components/database/components/field_types/number/NumberFieldActions'; import DateTimeFieldActions from '$app/components/database/components/field_types/date/DateTimeFieldActions'; -function FieldTypeMenuExtension({ field }: { field: Field }) { +function PropertyTypeMenuExtension({ field }: { field: Field }) { return useMemo(() => { switch (field.type) { case FieldType.SingleSelect: @@ -23,4 +23,4 @@ function FieldTypeMenuExtension({ field }: { field: Field }) { }, [field]); } -export default FieldTypeMenuExtension; +export default PropertyTypeMenuExtension; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeSelect.tsx similarity index 68% rename from frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeSelect.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeSelect.tsx index aa87eee94b..8226b2aad5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeSelect.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeSelect.tsx @@ -1,17 +1,17 @@ import React, { useRef, useState } from 'react'; -import { FieldTypeSvg } from '$app/components/database/components/field/FieldTypeSvg'; +import { ProppertyTypeSvg } from '$app/components/database/components/property/property_type/ProppertyTypeSvg'; import { MenuItem } from '@mui/material'; import { Field } from '$app/components/database/application'; import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; -import { FieldTypeMenu } from '$app/components/database/components/field/FieldTypeMenu'; +import { PropertyTypeMenu } from '$app/components/database/components/property/property_type/PropertyTypeMenu'; import { FieldType } from '@/services/backend'; -import { FieldTypeText } from '$app/components/database/components/field/FieldTypeText'; +import { PropertyTypeText } from '$app/components/database/components/property/property_type/PropertyTypeText'; interface Props { field: Field; onUpdateFieldType: (type: FieldType) => void; } -function FieldTypeSelect({ field, onUpdateFieldType }: Props) { +function PropertyTypeSelect({ field, onUpdateFieldType }: Props) { const [expanded, setExpanded] = useState(false); const ref = useRef(null); @@ -24,14 +24,14 @@ function FieldTypeSelect({ field, onUpdateFieldType }: Props) { }} className={'px-23 mx-0'} > - + - + {expanded && ( - { +export const PropertyTypeText = ({ type }: { type: FieldType }) => { const { t } = useTranslation(); const text = useMemo(() => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/ProppertyTypeSvg.tsx similarity index 93% rename from frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeSvg.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/ProppertyTypeSvg.tsx index d1d42646e7..e2710b06cc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeSvg.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/ProppertyTypeSvg.tsx @@ -23,7 +23,7 @@ export const FieldTypeSvgMap: Record [FieldType.CreatedTime]: LastEditedTimeSvg, }; -export const FieldTypeSvg: FC<{ type: FieldType, className?: string }> = memo(({ type, ...props }) => { +export const ProppertyTypeSvg: FC<{ type: FieldType; className?: string }> = memo(({ type, ...props }) => { const Svg = FieldTypeSvgMap[type]; return ; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortFieldsMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortFieldsMenu.tsx index cc4f28823d..a47d19bb37 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortFieldsMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortFieldsMenu.tsx @@ -1,6 +1,6 @@ import React, { FC, MouseEvent, useCallback } from 'react'; import { MenuProps } from '@mui/material'; -import FieldList from '$app/components/database/components/field/FieldList'; +import PropertiesList from '$app/components/database/components/property/PropertiesList'; import { Field, sortService } from '$app/components/database/application'; import { SortConditionPB } from '@/services/backend'; import { useTranslation } from 'react-i18next'; @@ -29,7 +29,7 @@ const SortFieldsMenu: FC< return ( - + ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortItem.tsx index 63b9b68ae5..f6e216f371 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortItem.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortItem.tsx @@ -2,7 +2,7 @@ import { IconButton, SelectChangeEvent, Stack } from '@mui/material'; import { FC, useCallback } from 'react'; import { ReactComponent as CloseSvg } from '$app/assets/close.svg'; import { Field, Sort, sortService } from '../../application'; -import { FieldSelect } from '../field'; +import { PropertySelect } from '../property'; import { SortConditionSelect } from './SortConditionSelect'; import { useViewId } from '@/appflowy_app/hooks'; import { SortConditionPB } from '@/services/backend'; @@ -44,7 +44,7 @@ export const SortItem: FC = ({ className, sort }) => { return ( - + { children?: React.ReactNode; index: number; value: number; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/Grid/Grid.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/Grid/Grid.tsx index 73e96c2fda..154c0d0444 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/Grid/Grid.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/Grid/Grid.tsx @@ -1,11 +1,6 @@ import { FC } from 'react'; -import { GridTable } from '../GridTable'; -import GridUIProvider from '$app/components/database/proxy/grid/ui_state/Provider'; +import { GridTable, GridTableProps } from '../GridTable'; -export const Grid: FC<{ isActivated: boolean; tableHeight: number }> = ({ isActivated, tableHeight }) => { - return ( - - - - ); +export const Grid: FC = (props) => { + return ; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCalculate/GridCalculate.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCalculate/GridCalculate.tsx index 829c9d855f..c6bcac90f9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCalculate/GridCalculate.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCalculate/GridCalculate.tsx @@ -1,23 +1,24 @@ import React from 'react'; import { useDatabaseVisibilityRows } from '$app/components/database'; import { Field } from '$app/components/database/application'; -import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow'; +import { DEFAULT_FIELD_WIDTH, GRID_ACTIONS_WIDTH } from '$app/components/database/grid/constants'; interface Props { field: Field; index: number; + getContainerRef?: () => React.RefObject; } function GridCalculate({ field, index }: Props) { const rowMetas = useDatabaseVisibilityRows(); const count = rowMetas.length; - const width = field.width ?? DEFAULT_FIELD_WIDTH; + const width = index === 0 ? GRID_ACTIONS_WIDTH : field.width ?? DEFAULT_FIELD_WIDTH; return (
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 d8a4727674..580643c99e 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 @@ -1,6 +1,75 @@ -import { FC } from 'react'; -import { Cell, CellProps } from '../../components'; +import React, { CSSProperties, memo } from 'react'; +import { GridColumn, RenderRow, RenderRowType } from '../constants'; +import GridNewRow from '$app/components/database/grid/GridNewRow/GridNewRow'; +import GridCalculate from '$app/components/database/grid/GridCalculate/GridCalculate'; +import { areEqual } from 'react-window'; +import { Cell } from '$app/components/database/components'; +import PrimaryCell from '$app/components/database/grid/GridCell/PrimaryCell'; -export const GridCell: FC = (props) => { - return ; +const getRenderRowKey = (row: RenderRow) => { + if (row.type === RenderRowType.Row) { + return `row:${row.data.meta.id}`; + } + + return row.type; }; + +interface GridCellProps { + row: RenderRow; + column: GridColumn; + columnIndex: number; + style: CSSProperties; + onEditRecord?: (rowId: string) => void; + getContainerRef?: () => React.RefObject; +} + +export const GridCell = memo(({ row, column, columnIndex, style, onEditRecord, getContainerRef }: GridCellProps) => { + const key = getRenderRowKey(row); + + const field = column.field; + + if (!field) return
; + + switch (row.type) { + case RenderRowType.Row: { + const renderRowCell = ; + + return ( +
+ {field.isPrimary ? ( + + {renderRowCell} + + ) : ( + renderRowCell + )} +
+ ); + } + + case RenderRowType.NewRow: + return ( +
+ +
+ ); + case RenderRowType.CalculateRow: + return ( +
+ +
+ ); + default: + return null; + } +}, areEqual); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/PrimaryCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/PrimaryCell.tsx new file mode 100644 index 0000000000..1b2a7f1606 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/PrimaryCell.tsx @@ -0,0 +1,51 @@ +import React, { Suspense, useMemo, useRef } from 'react'; +import { ReactComponent as OpenIcon } from '$app/assets/open.svg'; +import { IconButton } from '@mui/material'; + +import { useGridTableHoverState } from '$app/components/database/grid/GridRowActions/GridRowActions.hooks'; + +function PrimaryCell({ + onEditRecord, + icon, + getContainerRef, + rowId, + children, +}: { + rowId: string; + icon?: string; + onEditRecord?: (rowId: string) => void; + getContainerRef?: () => React.RefObject; + children?: React.ReactNode; +}) { + const cellRef = useRef(null); + + const containerRef = getContainerRef?.(); + const { hoverRowId } = useGridTableHoverState(containerRef); + + const showExpandIcon = useMemo(() => { + return hoverRowId === rowId; + }, [hoverRowId, rowId]); + + return ( +
+ {icon &&
{icon}
} + {children} + + {showExpandIcon && ( +
+ onEditRecord?.(rowId)} className={'h-6 w-6 text-sm'}> + + +
+ )} +
+
+ ); +} + +export default PrimaryCell; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridField.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridField.tsx index b092798bd3..aac2080793 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridField.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridField.tsx @@ -1,140 +1,194 @@ import { Button, Tooltip } from '@mui/material'; -import { DragEventHandler, FC, useCallback, useMemo, useState } from 'react'; +import { DragEventHandler, FC, HTMLAttributes, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { throttle } from '$app/utils/tool'; import { useViewId } from '$app/hooks'; import { DragItem, DropPosition, DragType, useDraggable, useDroppable, ScrollDirection } from '../../_shared'; import { fieldService, Field } from '../../application'; import { useDatabase } from '../../Database.hooks'; -import { FieldTypeSvg } from '$app/components/database/components/field'; -import { FieldMenu } from '../../components/field/FieldMenu'; +import { Property } from '$app/components/database/components/property'; import GridResizer from '$app/components/database/grid/GridField/GridResizer'; -import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow'; +import GridFieldMenu from '$app/components/database/grid/GridField/GridFieldMenu'; +import { areEqual } from 'react-window'; +import { useOpenMenu } from '$app/components/database/grid/GridStickyHeader/GridStickyHeader.hooks'; -export interface GridFieldProps { +export interface GridFieldProps extends HTMLAttributes { field: Field; + onOpenMenu?: (id: string) => void; + onCloseMenu?: (id: string) => void; + resizeColumnWidth?: (width: number) => void; + getScrollElement?: () => HTMLElement | null; } -export const GridField: FC = ({ field }) => { - const viewId = useViewId(); - const { fields } = useDatabase(); - const [menuAnchorEl, setMenuAnchorEl] = useState(null); - const [openTooltip, setOpenTooltip] = useState(false); - const [dropPosition, setDropPosition] = useState(DropPosition.Before); - const [fieldWidth, setFieldWidth] = useState(field.width || DEFAULT_FIELD_WIDTH); - const openMenu = Boolean(menuAnchorEl); - const handleClick = useCallback((e: React.MouseEvent) => { - setMenuAnchorEl(e.currentTarget); - }, []); +export const GridField: FC = memo( + ({ getScrollElement, resizeColumnWidth, onOpenMenu, onCloseMenu, field, ...props }) => { + const menuOpened = useOpenMenu(field.id); + const viewId = useViewId(); + const { fields } = useDatabase(); + const [openTooltip, setOpenTooltip] = useState(false); + const [propertyMenuOpened, setPropertyMenuOpened] = useState(false); + const [dropPosition, setDropPosition] = useState(DropPosition.Before); - const handleMenuClose = useCallback(() => { - setMenuAnchorEl(null); - }, []); + const handleTooltipOpen = useCallback(() => { + setOpenTooltip(true); + }, []); - const handleTooltipOpen = useCallback(() => { - setOpenTooltip(true); - }, []); + const handleTooltipClose = useCallback(() => { + setOpenTooltip(false); + }, []); - const handleTooltipClose = useCallback(() => { - setOpenTooltip(false); - }, []); + const draggingData = useMemo( + () => ({ + field, + }), + [field] + ); - const draggingData = useMemo( - () => ({ - field, - }), - [field] - ); + const { isDragging, attributes, listeners, setPreviewRef, previewRef } = useDraggable({ + type: DragType.Field, + data: draggingData, + scrollOnEdge: { + direction: ScrollDirection.Horizontal, + getScrollElement, + }, + }); - 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; - const onDragOver = useMemo(() => { - return throttle((event) => { - const element = previewRef.current; + if (!element) { + return; + } - if (!element) { + 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 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 fieldService.moveField(viewId, dragField.id, fromIndex, toIndex); + }, + [viewId, field, fields, dropPosition] + ); + + const { isOver, listeners: dropListeners } = useDroppable({ + accept: DragType.Field, + disabled: isDragging, + onDragOver, + onDrop, + }); + + const [menuAnchorPosition, setMenuAnchorPosition] = useState< + | { + top: number; + left: number; + } + | undefined + >(undefined); + + const open = Boolean(menuAnchorPosition) && menuOpened; + + const handleClick = useCallback(() => { + onOpenMenu?.(field.id); + }, [onOpenMenu, field.id]); + + const handleMenuClose = useCallback(() => { + onCloseMenu?.(field.id); + }, [onCloseMenu, field.id]); + + useEffect(() => { + if (!menuOpened) { + setMenuAnchorPosition(undefined); return; } - const { left, right } = element.getBoundingClientRect(); - const middle = (left + right) / 2; + const rect = previewRef.current?.getBoundingClientRect(); - setDropPosition(event.clientX < middle ? DropPosition.Before : DropPosition.After); - }, 20); - }, [previewRef]); - - const onDrop = useCallback( - ({ data }: DragItem) => { - const dragField = data.field as 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; + if (rect) { + setMenuAnchorPosition({ + top: rect.top + rect.height, + left: rect.left, + }); + } else { + setMenuAnchorPosition(undefined); } + }, [menuOpened, previewRef]); - void fieldService.moveField(viewId, dragField.id, fromIndex, toIndex); - }, - [viewId, field, fields, dropPosition] - ); + const handlePropertyMenuOpen = useCallback(() => { + setPropertyMenuOpened(true); + }, []); - const { isOver, listeners: dropListeners } = useDroppable({ - accept: DragType.Field, - disabled: isDragging, - onDragOver, - onDrop, - }); + const handlePropertyMenuClose = useCallback(() => { + setPropertyMenuOpened(false); + }, []); - return ( -
- - - - {openMenu && } -
- ); -}; + {isOver && ( +
+ )} + + + + {open && ( + + )} +
+ ); + }, + areEqual +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridFieldMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridFieldMenu.tsx new file mode 100644 index 0000000000..0dc5af12a6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridFieldMenu.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import { Field } from '$app/components/database/application'; +import PropertyNameInput from '$app/components/database/components/property/PropertyNameInput'; +import { MenuList, Portal } from '@mui/material'; +import PropertyActions, { FieldAction } from '$app/components/database/components/property/PropertyActions'; + +interface Props extends PopoverProps { + field: Field; + onOpenPropertyMenu?: () => void; + onOpenMenu?: (fieldId: string) => void; +} + +function GridFieldMenu({ field, onOpenPropertyMenu, onOpenMenu, ...props }: Props) { + return ( + + e.stopPropagation()} + {...props} + keepMounted={false} + > + + + { + if (action === FieldAction.EditProperty) { + onOpenPropertyMenu?.(); + } else if (newFieldId && (action === FieldAction.InsertLeft || action === FieldAction.InsertRight)) { + onOpenMenu?.(newFieldId); + } + + props.onClose?.({}, 'backdropClick'); + }} + fieldId={field.id} + /> + + + + ); +} + +export default GridFieldMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridNewField.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridNewField.tsx new file mode 100644 index 0000000000..10611fc446 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridNewField.tsx @@ -0,0 +1,35 @@ +import React, { useCallback } from 'react'; +import { useViewId } from '$app/hooks'; +import { useTranslation } from 'react-i18next'; +import { fieldService } from '$app/components/database/application'; +import { FieldType } from '@/services/backend'; +import Button from '@mui/material/Button'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; + +function GridNewField({ onInserted }: { onInserted?: (id: string) => void }) { + const viewId = useViewId(); + const { t } = useTranslation(); + + const handleClick = useCallback(async () => { + try { + const field = await fieldService.createField({ + viewId, + fieldType: FieldType.RichText, + }); + + onInserted?.(field.id); + } catch (e) { + // toast.error(t('grid.field.newPropertyFail')); + } + }, [onInserted, viewId]); + + return ( + <> + + + ); +} + +export default GridNewField; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridResizer.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridResizer.tsx index 6a834b6fab..bac219ee08 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridResizer.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridResizer.tsx @@ -28,14 +28,11 @@ function GridResizer({ field, onWidthChange }: GridResizerProps) { } setNewWidth(newWidth); + onWidthChange?.(newWidth); }, - [width] + [width, onWidthChange] ); - useEffect(() => { - onWidthChange?.(newWidth); - }, [newWidth, onWidthChange]); - useEffect(() => { if (!isResizing && width !== newWidth) { void fieldService.updateFieldSetting(viewId, fieldId, { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridNewRow/GridNewRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridNewRow/GridNewRow.tsx new file mode 100644 index 0000000000..cb057b87a2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridNewRow/GridNewRow.tsx @@ -0,0 +1,69 @@ +import React, { useCallback } from 'react'; +import { rowService } from '$app/components/database/application'; +import { useViewId } from '$app/hooks'; +import { t } from 'i18next'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; + +interface Props { + index: number; + startRowId?: string; + groupId?: string; + getContainerRef?: () => React.RefObject; +} + +const CSS_HIGHLIGHT_PROPERTY = 'bg-content-blue-50'; + +function GridNewRow({ index, startRowId, groupId, getContainerRef }: Props) { + const viewId = useViewId(); + + const handleClick = useCallback(() => { + void rowService.createRow(viewId, { + startRowId, + groupId, + }); + }, [viewId, groupId, startRowId]); + + const toggleCssProperty = useCallback( + (status: boolean) => { + const container = getContainerRef?.()?.current; + + if (!container) return; + + const newRowCells = container.querySelectorAll('.grid-new-row'); + + newRowCells.forEach((cell) => { + if (status) { + cell.classList.add(CSS_HIGHLIGHT_PROPERTY); + } else { + cell.classList.remove(CSS_HIGHLIGHT_PROPERTY); + } + }); + }, + [getContainerRef] + ); + + return ( +
{ + toggleCssProperty(true); + }} + onMouseLeave={() => { + toggleCssProperty(false); + }} + onClick={handleClick} + className={'grid-new-row flex grow'} + > + + + {t('grid.row.newRow')} + +
+ ); +} + +export default GridNewRow; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridOverlay/GridTableOverlay.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridOverlay/GridTableOverlay.tsx new file mode 100644 index 0000000000..6c1ecc95df --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridOverlay/GridTableOverlay.tsx @@ -0,0 +1,44 @@ +import React, { useEffect, useState } from 'react'; +import GridRowContextMenu from '$app/components/database/grid/GridRowActions/GridRowContextMenu'; +import GridRowActions from '$app/components/database/grid/GridRowActions/GridRowActions'; + +import { useGridTableHoverState } from '$app/components/database/grid/GridRowActions/GridRowActions.hooks'; + +function GridTableOverlay({ + containerRef, + getScrollElement, +}: { + containerRef: React.MutableRefObject; + getScrollElement: () => HTMLDivElement | null; +}) { + const [hoverRowTop, setHoverRowTop] = useState(); + + const { hoverRowId } = useGridTableHoverState(containerRef); + + useEffect(() => { + const container = containerRef.current; + + if (!container) return; + + const cell = container.querySelector(`[data-key="row:${hoverRowId}"]`); + + if (!cell) return; + const top = (cell as HTMLDivElement).style.top; + + setHoverRowTop(top); + }, [containerRef, hoverRowId]); + + return ( +
+ + +
+ ); +} + +export default GridTableOverlay; 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 deleted file mode 100644 index cd4c5a53f9..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCalculateRow.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { useDatabaseVisibilityFields } from '$app/components/database'; -import GridCalculate from '$app/components/database/grid/GridCalculate/GridCalculate'; - -function GridCalculateRow() { - const fields = useDatabaseVisibilityFields(); - - return ( -
- {fields.map((field, index) => { - return ; - })} -
- ); -} - -export default GridCalculateRow; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks.ts deleted file mode 100644 index e3dce4d803..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useGridUIStateDispatcher, useGridUIStateSelector } from '$app/components/database/proxy/grid/ui_state/actions'; -import { useCallback, useMemo, useState } from 'react'; - -export function useGridRowActionsDisplay(rowId: string) { - const { hoverRowId, isActivated } = useGridUIStateSelector(); - const hover = useMemo(() => { - return isActivated && hoverRowId === rowId; - }, [hoverRowId, rowId, isActivated]); - - const { setRowHover } = useGridUIStateDispatcher(); - - const onMouseEnter = useCallback(() => { - setRowHover(rowId); - }, [setRowHover, rowId]); - - const onMouseLeave = useCallback(() => { - if (hover) { - setRowHover(null); - } - }, [setRowHover, hover]); - - return { - onMouseEnter, - onMouseLeave, - hover, - }; -} - -export const useGridRowContextMenu = () => { - const [position, setPosition] = useState<{ left: number; top: number } | undefined>(); - - const isContextMenuOpen = useMemo(() => { - return !!position; - }, [position]); - - const closeContextMenu = useCallback(() => { - setPosition(undefined); - }, []); - - const openContextMenu = useCallback((event: MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - - setPosition({ - left: event.clientX, - top: event.clientY, - }); - }, []); - - return { - isContextMenuOpen, - closeContextMenu, - openContextMenu, - position, - }; -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.tsx deleted file mode 100644 index 98acab36f2..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { Virtualizer } from '@tanstack/react-virtual'; -import { Portal } from '@mui/material'; -import { DragEventHandler, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { throttle } from '$app/utils/tool'; -import { useViewId } from '$app/hooks'; -import { useDatabaseVisibilityFields } from '../../../Database.hooks'; -import { rowService, RowMeta } from '../../../application'; -import { - DragItem, - DragType, - DropPosition, - VirtualizedList, - useDraggable, - useDroppable, - ScrollDirection, -} from '../../../_shared'; -import { GridCell } from '../../GridCell'; -import { GridCellRowActions } from './GridCellRowActions'; -import { - useGridRowActionsDisplay, - useGridRowContextMenu, -} from '$app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks'; -import GridCellRowContextMenu from '$app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu'; -import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow'; - -export interface GridCellRowProps { - rowMeta: RowMeta; - virtualizer: Virtualizer; - getPrevRowId: (id: string) => string | null; -} - -export const GridCellRow: FC = ({ rowMeta, virtualizer, getPrevRowId }) => { - const rowId = rowMeta.id; - const viewId = useViewId(); - const ref = useRef(null); - const { onMouseLeave, onMouseEnter, hover } = useGridRowActionsDisplay(rowId); - const { - isContextMenuOpen, - closeContextMenu, - openContextMenu, - position: contextMenuPosition, - } = useGridRowContextMenu(); - const fields = useDatabaseVisibilityFields(); - - const [dropPosition, setDropPosition] = useState(DropPosition.Before); - const dragData = useMemo( - () => ({ - rowMeta, - }), - [rowMeta] - ); - - const { - isDragging, - attributes: dragAttributes, - listeners: dragListeners, - 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 rowService.moveRow(viewId, (data.rowMeta as RowMeta).id, rowMeta.id); - }, - [viewId, rowMeta.id] - ); - - const { isOver, listeners: dropListeners } = useDroppable({ - accept: DragType.Row, - disabled: isDragging, - onDragOver, - onDrop, - }); - - useEffect(() => { - const element = ref.current; - - if (!element) { - return; - } - - element.addEventListener('contextmenu', openContextMenu); - return () => { - element.removeEventListener('contextmenu', openContextMenu); - }; - }, [openContextMenu]); - - return ( -
-
- { - const field = fields[index]; - const icon = field.isPrimary ? rowMeta.icon : undefined; - const documentId = field.isPrimary ? rowMeta.documentId : undefined; - - return ; - }} - /> -
- {isOver && ( -
- )} -
- - - {isContextMenuOpen && ( - - )} - -
- ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowActions.tsx deleted file mode 100644 index f663fa9c2e..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowActions.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { IconButton, Tooltip } from '@mui/material'; -import { DragEventHandler, FC, HTMLAttributes, PropsWithChildren, useCallback, useRef, useState } from 'react'; -import { t } from 'i18next'; -import { ReactComponent as AddSvg } from '$app/assets/add.svg'; -import { useViewId } from '$app/hooks'; -import { rowService } from '../../../application'; -import { ReactComponent as DragSvg } from '$app/assets/drag.svg'; -import GridCellRowMenu from '$app/components/database/grid/GridRow/GridCellRow/GridCellRowMenu'; -import Popover from '@mui/material/Popover'; - -export interface GridCellRowActionsProps extends HTMLAttributes { - rowId: string; - getPrevRowId: (id: string) => string | null; - dragProps: { - draggable?: boolean; - onDragStart?: DragEventHandler; - onDragEnd?: DragEventHandler; - }; - isHidden?: boolean; -} - -export const GridCellRowActions: FC> = ({ - isHidden, - rowId, - getPrevRowId, - className, - dragProps: { draggable, onDragStart, onDragEnd }, - ...props -}) => { - const viewId = useViewId(); - const ref = useRef(null); - const [menuPosition, setMenuPosition] = useState<{ - top: number; - left: number; - }>(); - const handleInsertRecordBelow = useCallback(() => { - void rowService.createRow(viewId, { - startRowId: rowId, - }); - }, [viewId, rowId]); - - const handleOpenMenu = (e: React.MouseEvent) => { - const target = e.target as HTMLButtonElement; - const rect = target.getBoundingClientRect(); - - setMenuPosition({ - top: rect.top + rect.height / 2, - left: rect.left + rect.width, - }); - }; - - const handleCloseMenu = () => { - setMenuPosition(undefined); - }; - - const openMenu = !!menuPosition; - - return ( - <> - {!isHidden && ( -
- - - - - - - - - - -
- )} - - {openMenu && ( - - - - )} - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu.tsx deleted file mode 100644 index f045991c7c..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import GridCellRowMenu from '$app/components/database/grid/GridRow/GridCellRow/GridCellRowMenu'; -import Popover from '@mui/material/Popover'; - -interface Props { - open: boolean; - onClose: () => void; - anchorPosition?: { - top: number; - left: number; - }; - rowId: string; - getPrevRowId: (id: string) => string | null; -} - -function GridCellRowContextMenu({ open, anchorPosition, onClose, rowId, getPrevRowId }: Props) { - return ( - - { - onClose(); - }} - /> - - ); -} - -export default GridCellRowContextMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/index.ts deleted file mode 100644 index 2b5749c5aa..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './GridCellRow'; 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 deleted file mode 100644 index 6b9fb2b2bd..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useDatabaseVisibilityFields } from '../../Database.hooks'; -import { GridField } from '../GridField'; -import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow/constants'; -import React from 'react'; -import NewProperty from '$app/components/database/components/edit_record/record_properties/NewProperty'; - -export const GridFieldRow = () => { - const fields = useDatabaseVisibilityFields(); - - return ( - <> -
-
- {fields.map((field) => { - return ; - })} -
- -
- -
-
- - ); -}; 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 deleted file mode 100644 index 051adde067..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridNewRow.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { FC, useCallback } from 'react'; -import { t } from 'i18next'; -import { Button } from '@mui/material'; -import { ReactComponent as AddSvg } from '$app/assets/add.svg'; -import { useViewId } from '$app/hooks'; -import { rowService } from '../../application'; - -export interface GridNewRowProps { - startRowId?: string; - groupId?: string; -} - -export const GridNewRow: FC = ({ startRowId, groupId }) => { - const viewId = useViewId(); - - const handleClick = useCallback(() => { - void rowService.createRow(viewId, { - startRowId, - groupId, - }); - }, [viewId, groupId, startRowId]); - - return ( -
- -
- ); -}; 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 deleted file mode 100644 index bec95507c7..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Virtualizer } from '@tanstack/react-virtual'; -import React, { FC } from 'react'; -import { RenderRow, RenderRowType } from './constants'; -import { GridCellRow } from './GridCellRow'; -import { GridNewRow } from './GridNewRow'; -import { GridFieldRow } from '$app/components/database/grid/GridRow/GridFieldRow'; -import GridCalculateRow from '$app/components/database/grid/GridRow/GridCalculateRow'; - -export interface GridRowProps { - row: RenderRow; - virtualizer: Virtualizer; - getPrevRowId: (id: string) => string | null; -} - -export const GridRow: FC = React.memo(({ row, virtualizer, getPrevRowId }) => { - switch (row.type) { - case RenderRowType.Fields: - return ; - case RenderRowType.Row: - return ; - case RenderRowType.NewRow: - return ; - case RenderRowType.CalculateRow: - return ; - default: - return null; - } -}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/index.ts deleted file mode 100644 index 74cbdd3fa7..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './GridRow'; -export * from './constants'; \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowActions.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowActions.hooks.ts new file mode 100644 index 0000000000..12458801e3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowActions.hooks.ts @@ -0,0 +1,219 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useViewId } from '$app/hooks'; +import { useGetPrevRowId } from '$app/components/database'; +import { rowService } from '$app/components/database/application'; +import { autoScrollOnEdge, ScrollDirection } from '$app/components/database/_shared/dnd/utils'; + +export function getCellsWithRowId(rowId: string, container: HTMLDivElement) { + return Array.from(container.querySelectorAll(`[data-key^="row:${rowId}"]`)); +} + +const SELECTED_ROW_CSS_PROPERTY = 'bg-content-blue-50'; + +export function toggleProperty( + container: HTMLDivElement, + rowId: string, + status: boolean, + property = SELECTED_ROW_CSS_PROPERTY +) { + const rowColumns = getCellsWithRowId(rowId, container); + + rowColumns.forEach((column, index) => { + if (index === 0) return; + if (status) { + column.classList.add(property); + } else { + column.classList.remove(property); + } + }); +} + +function createVirtualDragElement(rowId: string, container: HTMLDivElement) { + const cells = getCellsWithRowId(rowId, container); + + const cell = cells[0] as HTMLDivElement; + + if (!cell) return null; + + const rect = cell.getBoundingClientRect(); + + const row = document.createElement('div'); + + row.style.display = 'flex'; + row.style.position = 'absolute'; + row.style.top = `${rect.top}px`; + row.style.left = `${rect.left + 64}px`; + row.style.background = 'var(--content-blue-50)'; + cells.forEach((cell) => { + const node = cell.cloneNode(true) as HTMLDivElement; + + if (!node.classList.contains('grid-cell')) return; + + node.style.top = ''; + node.style.position = ''; + node.style.left = ''; + node.style.width = (cell as HTMLDivElement).style.width; + node.style.height = (cell as HTMLDivElement).style.height; + node.className = 'flex items-center'; + row.appendChild(node); + }); + + document.body.appendChild(row); + return row; +} + +export function useDraggableGridRow( + rowId: string, + containerRef: React.RefObject, + getScrollElement: () => HTMLDivElement | null +) { + const [isDragging, setIsDragging] = useState(false); + const dropRowIdRef = useRef(undefined); + const previewRef = useRef(); + const viewId = useViewId(); + const getPrevRowId = useGetPrevRowId(); + const onDragStart = useCallback( + (e: React.DragEvent) => { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.dropEffect = 'move'; + const container = containerRef.current; + + if (container) { + const row = createVirtualDragElement(rowId, container); + + if (row) { + previewRef.current = row; + e.dataTransfer.setDragImage(row, 0, 0); + } + } + + const scrollParent = getScrollElement(); + + if (scrollParent) { + autoScrollOnEdge({ + element: scrollParent, + direction: ScrollDirection.Vertical, + }); + } + + setIsDragging(true); + }, + [containerRef, rowId, getScrollElement] + ); + + useEffect(() => { + if (!isDragging) { + if (previewRef.current) { + const row = previewRef.current; + + previewRef.current = undefined; + row?.remove(); + } + + return; + } + + const container = containerRef.current; + + if (!container) { + return; + } + + const onDragOver = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + const target = e.target as HTMLElement; + const cell = target.closest('[data-key]'); + const rowId = cell?.getAttribute('data-key')?.split(':')[1]; + + const oldRowId = dropRowIdRef.current; + + if (oldRowId) { + toggleProperty(container, oldRowId, false); + } + + if (!rowId) return; + + const rowColumns = getCellsWithRowId(rowId, container); + + dropRowIdRef.current = rowId; + if (!rowColumns.length) return; + + toggleProperty(container, rowId, true); + }; + + const onDragEnd = () => { + const oldRowId = dropRowIdRef.current; + + if (oldRowId) { + toggleProperty(container, oldRowId, false); + } + + dropRowIdRef.current = undefined; + setIsDragging(false); + }; + + const onDrop = async (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const dropRowId = dropRowIdRef.current; + + if (dropRowId) { + void rowService.moveRow(viewId, rowId, dropRowId); + } + + setIsDragging(false); + container.removeEventListener('dragover', onDragOver); + container.removeEventListener('dragend', onDragEnd); + container.removeEventListener('drop', onDrop); + }; + + container.addEventListener('dragover', onDragOver); + container.addEventListener('dragend', onDragEnd); + container.addEventListener('drop', onDrop); + }, [containerRef, getPrevRowId, isDragging, rowId, viewId]); + + return { + isDragging, + onDragStart, + }; +} + +export function useGridTableHoverState(containerRef?: React.RefObject) { + const [hoverRowId, setHoverRowId] = useState(undefined); + + useEffect(() => { + const container = containerRef?.current; + + if (!container) return; + const onMouseMove = (e: MouseEvent) => { + const target = e.target as HTMLElement; + const cell = target.closest('[data-key]'); + + if (!cell) { + return; + } + + const hoverRowId = cell.getAttribute('data-key')?.split(':')[1]; + + setHoverRowId(hoverRowId); + }; + + const onMouseLeave = () => { + setHoverRowId(undefined); + }; + + container.addEventListener('mousemove', onMouseMove); + container.addEventListener('mouseleave', onMouseLeave); + + return () => { + container.removeEventListener('mousemove', onMouseMove); + container.removeEventListener('mouseleave', onMouseLeave); + }; + }, [containerRef]); + + return { + hoverRowId, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowActions.tsx new file mode 100644 index 0000000000..eac7e9a64d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowActions.tsx @@ -0,0 +1,98 @@ +import React, { useCallback, useState } from 'react'; +import { IconButton, Tooltip } from '@mui/material'; +import { t } from 'i18next'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { GRID_ACTIONS_WIDTH } from '$app/components/database/grid/constants'; +import { rowService } from '$app/components/database/application'; +import { useViewId } from '$app/hooks'; +import GridRowDragButton from '$app/components/database/grid/GridRowActions/GridRowDragButton'; +import GridRowMenu from '$app/components/database/grid/GridRowActions/GridRowMenu'; + +function GridRowActions({ + rowId, + rowTop, + containerRef, + getScrollElement, +}: { + rowId?: string; + rowTop?: string; + containerRef: React.MutableRefObject; + getScrollElement: () => HTMLDivElement | null; +}) { + const viewId = useViewId(); + const [menuRowId, setMenuRowId] = useState(undefined); + const [menuPosition, setMenuPosition] = useState< + | { + top: number; + left: number; + } + | undefined + >(undefined); + + const openMenu = Boolean(menuPosition); + + const handleInsertRecordBelow = useCallback(() => { + void rowService.createRow(viewId, { + startRowId: rowId, + }); + }, [viewId, rowId]); + + const handleOpenMenu = (e: React.MouseEvent) => { + const target = e.target as HTMLButtonElement; + const rect = target.getBoundingClientRect(); + + setMenuRowId(rowId); + setMenuPosition({ + top: rect.top + rect.height / 2, + left: rect.left + rect.width, + }); + }; + + const handleCloseMenu = useCallback(() => { + setMenuPosition(undefined); + setMenuRowId(undefined); + }, []); + + return ( + <> + {rowId && rowTop && ( +
+ + + + + + +
+ )} + {openMenu && menuRowId && ( + + )} + + ); +} + +export default GridRowActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowContextMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowContextMenu.tsx new file mode 100644 index 0000000000..0734b345c1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowContextMenu.tsx @@ -0,0 +1,64 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import GridRowMenu from './GridRowMenu'; +import { toggleProperty } from './GridRowActions.hooks'; + +function GridRowContextMenu({ + containerRef, + hoverRowId, +}: { + hoverRowId?: string; + containerRef: React.MutableRefObject; +}) { + const [position, setPosition] = useState<{ left: number; top: number } | undefined>(); + + const [rowId, setRowId] = useState(); + + const isContextMenuOpen = useMemo(() => { + return !!position; + }, [position]); + + const closeContextMenu = useCallback(() => { + setPosition(undefined); + const container = containerRef.current; + + if (!container || !rowId) return; + toggleProperty(container, rowId, false); + setRowId(undefined); + }, [rowId, containerRef]); + + const openContextMenu = useCallback( + (event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + const container = containerRef.current; + + if (!container || !hoverRowId) return; + toggleProperty(container, hoverRowId, true); + setRowId(hoverRowId); + setPosition({ + left: event.clientX, + top: event.clientY, + }); + }, + [containerRef, hoverRowId] + ); + + useEffect(() => { + const container = containerRef.current; + + if (!container) { + return; + } + + container.addEventListener('contextmenu', openContextMenu); + return () => { + container.removeEventListener('contextmenu', openContextMenu); + }; + }, [containerRef, openContextMenu]); + + return isContextMenuOpen && rowId ? ( + + ) : null; +} + +export default GridRowContextMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowDragButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowDragButton.tsx new file mode 100644 index 0000000000..1ebb018da9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowDragButton.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useDraggableGridRow } from './GridRowActions.hooks'; +import { IconButton, Tooltip } from '@mui/material'; +import { ReactComponent as DragSvg } from '$app/assets/drag.svg'; +import { useTranslation } from 'react-i18next'; + +function GridRowDragButton({ + rowId, + containerRef, + onClick, + getScrollElement, +}: { + rowId: string; + onClick?: (e: React.MouseEvent) => void; + containerRef: React.MutableRefObject; + getScrollElement: () => HTMLDivElement | null; +}) { + const { t } = useTranslation(); + + const { onDragStart } = useDraggableGridRow(rowId, containerRef, getScrollElement); + + return ( + + + + + + ); +} + +export default GridRowDragButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowMenu.tsx similarity index 61% rename from frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowMenu.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowMenu.tsx index 55664ac48b..9e3a6b8ff2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRowActions/GridRowMenu.tsx @@ -1,18 +1,14 @@ import React, { useCallback } from 'react'; -import { MenuList, MenuItem, Icon } from '@mui/material'; -import { useTranslation } from 'react-i18next'; import { ReactComponent as UpSvg } from '$app/assets/up.svg'; import { ReactComponent as AddSvg } from '$app/assets/add.svg'; import { ReactComponent as DelSvg } from '$app/assets/delete.svg'; import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import { useGetPrevRowId } from '$app/components/database'; +import { useViewId } from '$app/hooks'; +import { useTranslation } from 'react-i18next'; import { rowService } from '$app/components/database/application'; -import { useViewId } from '@/appflowy_app/hooks/ViewId.hooks'; - -interface Props { - rowId: string; - getPrevRowId: (id: string) => string | null; - onClickItem: (label: string) => void; -} +import { Icon, MenuItem, MenuList } from '@mui/material'; interface Option { label: string; @@ -21,7 +17,13 @@ interface Option { divider?: boolean; } -function GridCellRowMenu({ rowId, getPrevRowId, onClickItem }: Props) { +interface Props extends PopoverProps { + rowId: string; +} + +function GridRowMenu({ rowId, ...props }: Props) { + const getPrevRowId = useGetPrevRowId(); + const viewId = useViewId(); const { t } = useTranslation(); @@ -74,23 +76,30 @@ function GridCellRowMenu({ rowId, getPrevRowId, onClickItem }: Props) { ]; return ( - - {options.map((option) => ( -
- {option.divider &&
} - { - option.onClick(); - onClickItem(option.label); - }} - > - {option.icon} - {option.label} - -
- ))} - + + + {options.map((option) => ( +
+ {option.divider &&
} + { + option.onClick(); + props.onClose?.({}, 'backdropClick'); + }} + > + {option.icon} + {option.label} + +
+ ))} + + ); } -export default GridCellRowMenu; +export default GridRowMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridStickyHeader/GridStickyHeader.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridStickyHeader/GridStickyHeader.hooks.ts new file mode 100644 index 0000000000..ac5c0688b9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridStickyHeader/GridStickyHeader.hooks.ts @@ -0,0 +1,9 @@ +import { createContext, useContext } from 'react'; + +export const OpenMenuContext = createContext(null); + +export const useOpenMenu = (id: string) => { + const context = useContext(OpenMenuContext); + + return context === id; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridStickyHeader/GridStickyHeader.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridStickyHeader/GridStickyHeader.tsx new file mode 100644 index 0000000000..2b67a4fe6f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridStickyHeader/GridStickyHeader.tsx @@ -0,0 +1,102 @@ +import React, { useCallback, useState } from 'react'; +import { GridChildComponentProps, VariableSizeGrid as Grid } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { useGridColumn } from '$app/components/database/grid/GridTable/GridTable.hooks'; +import { GridField } from '$app/components/database/grid/GridField'; +import NewProperty from '$app/components/database/components/property/NewProperty'; +import { GridColumn, GridColumnType } from '$app/components/database/grid/constants'; +import { OpenMenuContext } from '$app/components/database/grid/GridStickyHeader/GridStickyHeader.hooks'; + +const GridStickyHeader = React.forwardRef< + Grid | null, + { columns: GridColumn[]; getScrollElement?: () => HTMLDivElement | null } +>(({ columns, getScrollElement }, ref) => { + const { columnWidth, resizeColumnWidth } = useGridColumn( + columns, + ref as React.MutableRefObject | null> + ); + + const [openMenuId, setOpenMenuId] = useState(null); + + const handleOpenMenu = useCallback((id: string) => { + setOpenMenuId(id); + }, []); + + const handleCloseMenu = useCallback((id: string) => { + setOpenMenuId((prev) => { + if (prev === id) { + return null; + } + + return prev; + }); + }, []); + + const Cell = useCallback( + ({ columnIndex, style }: GridChildComponentProps) => { + const column = columns[columnIndex]; + + if (column.type === GridColumnType.NewProperty) { + const width = (style.width || 0) as number; + + return ( +
+ +
+ ); + } + + if (column.type === GridColumnType.Action) { + return
; + } + + const field = column.field; + + if (!field) return
; + + return ( + resizeColumnWidth(columnIndex, width)} + field={field} + getScrollElement={getScrollElement} + /> + ); + }, + [columns, handleCloseMenu, handleOpenMenu, resizeColumnWidth, getScrollElement] + ); + + return ( + + + {({ height, width }: { height: number; width: number }) => { + return ( + 36} + rowCount={1} + columnCount={columns.length} + columnWidth={columnWidth} + ref={ref} + style={{ overflowX: 'hidden', overscrollBehavior: 'none' }} + > + {Cell} + + ); + }} + + + ); +}); + +export default GridStickyHeader; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.hooks.ts new file mode 100644 index 0000000000..3d534fbd3e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.hooks.ts @@ -0,0 +1,58 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { DEFAULT_FIELD_WIDTH, GRID_ACTIONS_WIDTH, GridColumn } from '$app/components/database/grid/constants'; +import { VariableSizeGrid as Grid } from 'react-window'; + +export function useGridRow() { + const rowHeight = useCallback(() => { + return 36; + }, []); + + return { + rowHeight, + }; +} + +export function useGridColumn(columns: GridColumn[], ref: React.RefObject | null>) { + const [columnWidths, setColumnWidths] = useState([]); + + useEffect(() => { + setColumnWidths( + columns.map((field, index) => (index === 0 ? GRID_ACTIONS_WIDTH : field.width || DEFAULT_FIELD_WIDTH)) + ); + ref.current?.resetAfterColumnIndex(0); + }, [columns, ref]); + + const resizeColumnWidth = useCallback( + (index: number, width: number) => { + setColumnWidths((columnWidths) => { + if (columnWidths[index] === width) { + return columnWidths; + } + + const newColumnWidths = [...columnWidths]; + + newColumnWidths[index] = width; + + return newColumnWidths; + }); + + if (ref.current) { + ref.current.resetAfterColumnIndex(index); + } + }, + [ref] + ); + + const columnWidth = useCallback( + (index: number) => { + if (index === 0) return GRID_ACTIONS_WIDTH; + return columnWidths[index] || DEFAULT_FIELD_WIDTH; + }, + [columnWidths] + ); + + return { + columnWidth, + resizeColumnWidth, + }; +} 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 7b8c5e66ac..c0bede08ee 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,85 +1,132 @@ -import { useVirtualizer } from '@tanstack/react-virtual'; import React, { FC, useCallback, useMemo, useRef } from 'react'; import { RowMeta } from '../../application'; import { useDatabaseVisibilityFields, useDatabaseVisibilityRows } from '../../Database.hooks'; -import { VirtualizedList } from '../../_shared'; -import { DEFAULT_FIELD_WIDTH, GridRow, RenderRow, RenderRowType, rowMetasToRenderRow } from '../GridRow'; +import { fieldsToColumns, GridColumn, RenderRow, RenderRowType, rowMetasToRenderRow } from '../constants'; import { CircularProgress } from '@mui/material'; +import { GridChildComponentProps, GridOnScrollProps, VariableSizeGrid as Grid } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { GridCell } from '$app/components/database/grid/GridCell'; +import { useGridColumn, useGridRow } from '$app/components/database/grid/GridTable/GridTable.hooks'; +import GridStickyHeader from '$app/components/database/grid/GridStickyHeader/GridStickyHeader'; +import GridTableOverlay from '$app/components/database/grid/GridOverlay/GridTableOverlay'; +import ReactDOM from 'react-dom'; -const getRenderRowKey = (row: RenderRow) => { - if (row.type === RenderRowType.Row) { - return `row:${row.data.meta.id}`; - } +export interface GridTableProps { + onEditRecord: (rowId: string) => void; +} - return row.type; -}; - -export const GridTable: FC<{ tableHeight: number }> = React.memo(({ tableHeight }) => { - const verticalScrollElementRef = useRef(null); - const horizontalScrollElementRef = useRef(null); +export const GridTable: FC = React.memo(({ onEditRecord }) => { const rowMetas = useDatabaseVisibilityRows(); const fields = useDatabaseVisibilityFields(); const renderRows = useMemo(() => rowMetasToRenderRow(rowMetas as RowMeta[]), [rowMetas]); + const columns = useMemo(() => fieldsToColumns(fields), [fields]); + const ref = useRef>(null); + const { columnWidth } = useGridColumn(columns, ref); + const { rowHeight } = useGridRow(); - const rowVirtualizer = useVirtualizer({ - count: renderRows.length, - overscan: 10, - getItemKey: (i) => getRenderRowKey(renderRows[i]), - getScrollElement: () => verticalScrollElementRef.current, - estimateSize: () => 37, - }); + const getItemKey = useCallback( + ({ columnIndex, rowIndex }: { columnIndex: number; rowIndex: number }) => { + const row = renderRows[rowIndex]; + const column = columns[columnIndex]; - const columnVirtualizer = useVirtualizer({ - horizontal: true, - count: fields.length, - overscan: 5, - getItemKey: (i) => fields[i].id, - getScrollElement: () => horizontalScrollElementRef.current, - estimateSize: (i) => { - return fields[i].width ?? DEFAULT_FIELD_WIDTH; - }, - }); + const field = column.field; - const getPrevRowId = useCallback( - (id: string) => { - const index = rowMetas.findIndex((rowMeta) => rowMeta.id === id); + if (row.type === RenderRowType.Row) { + if (field) { + return `${row.data.meta.id}:${field.id}`; + } - if (index === 0) { - return null; + return `${row.data.meta.id}:${column.type}`; } - return rowMetas[index - 1].id; + if (field) { + return `${row.type}:${field.id}`; + } + + return `${row.type}:${column.type}`; }, - [rowMetas] + [columns, renderRows] ); + const getContainerRef = useCallback(() => { + return containerRef; + }, []); + + const Cell = useCallback( + ({ columnIndex, rowIndex, style }: GridChildComponentProps) => { + const row = renderRows[rowIndex]; + const column = columns[columnIndex]; + + return ( + + ); + }, + [columns, getContainerRef, renderRows, onEditRecord] + ); + + const staticGrid = useRef | null>(null); + + const onScroll = useCallback(({ scrollLeft, scrollUpdateWasRequested }: GridOnScrollProps) => { + if (!scrollUpdateWasRequested) { + staticGrid.current?.scrollTo({ scrollLeft, scrollTop: 0 }); + } + }, []); + + const containerRef = useRef(null); + const scrollElementRef = useRef(null); + + const getScrollElement = useCallback(() => { + return scrollElementRef.current; + }, []); + return ( -
+
{fields.length === 0 && (
)} -
{ - verticalScrollElementRef.current = e; - horizontalScrollElementRef.current = e; - }} - > - ( - +
+ +
+ +
+ + {({ height, width }: { height: number; width: number }) => ( + + {Cell} + )} - /> + + {containerRef.current + ? ReactDOM.createPortal( + , + containerRef.current + ) + : null}
); 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/constants.ts similarity index 62% rename from frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts rename to frontend/appflowy_tauri/src/appflowy_app/components/database/grid/constants.ts index ba0b0ea966..d801fa9847 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/constants.ts @@ -1,7 +1,9 @@ -import { RowMeta } from '../../application'; +import { Field, RowMeta } from '../application'; export const GridCalculateCountHeight = 40; +export const GRID_ACTIONS_WIDTH = 64; + export const DEFAULT_FIELD_WIDTH = 150; export enum RenderRowType { @@ -36,11 +38,26 @@ export interface NewRenderRow { export type RenderRow = FieldRenderRow | CellRenderRow | NewRenderRow | CalculateRenderRow; -export const rowMetasToRenderRow = (rowMetas: RowMeta[]): RenderRow[] => { +export const fieldsToColumns = (fields: Field[]): GridColumn[] => { return [ { - type: RenderRowType.Fields, + type: GridColumnType.Action, + width: GRID_ACTIONS_WIDTH, }, + ...fields.map((field) => ({ + field, + width: field.width || DEFAULT_FIELD_WIDTH, + type: GridColumnType.Field, + })), + { + type: GridColumnType.NewProperty, + width: DEFAULT_FIELD_WIDTH, + }, + ]; +}; + +export const rowMetasToRenderRow = (rowMetas: RowMeta[]): RenderRow[] => { + return [ ...rowMetas.map((rowMeta) => ({ type: RenderRowType.Row, data: { @@ -58,3 +75,15 @@ export const rowMetasToRenderRow = (rowMetas: RowMeta[]): RenderRow[] => { }, ]; }; + +export enum GridColumnType { + Action, + Field, + NewProperty, +} + +export interface GridColumn { + field?: Field; + width: number; + type: GridColumnType; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/Provider.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/Provider.tsx deleted file mode 100644 index b462476494..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/Provider.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React, { useEffect } from 'react'; -import { GridUIContext, useProxyGridUIState } from '$app/components/database/proxy/grid/ui_state/actions'; - -function GridUIProvider({ children, isActivated }: { children: React.ReactNode; isActivated: boolean }) { - const context = useProxyGridUIState(); - - useEffect(() => { - context.isActivated = isActivated; - }, [isActivated, context]); - - return {children}; -} - -export default GridUIProvider; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/actions.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/actions.ts deleted file mode 100644 index 65ecee0570..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/actions.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useMemo, useContext, createContext, useCallback } from 'react'; -import { proxy, useSnapshot } from 'valtio'; - -export interface GridUIContextState { - hoverRowId: string | null; - isActivated: boolean; -} - -const initialUIState: GridUIContextState = { - hoverRowId: null, - isActivated: false, -}; - -function proxyGridUIState(state: GridUIContextState) { - return proxy(state); -} - -export const GridUIContext = createContext(proxyGridUIState(initialUIState)); - -export function useProxyGridUIState() { - const context = useMemo(() => { - return proxyGridUIState({ - ...initialUIState, - }); - }, []); - - return context; -} - -export function useGridUIStateSelector() { - return useSnapshot(useContext(GridUIContext)); -} - -export function useGridUIStateDispatcher() { - const context = useContext(GridUIContext); - const setRowHover = useCallback( - (rowId: string | null) => { - context.hoverRowId = rowId; - }, - [context] - ); - - return { - setRowHover, - }; -} diff --git a/frontend/appflowy_tauri/src/styles/template.css b/frontend/appflowy_tauri/src/styles/template.css index d4c82aa0ab..d32713f88a 100644 --- a/frontend/appflowy_tauri/src/styles/template.css +++ b/frontend/appflowy_tauri/src/styles/template.css @@ -149,3 +149,6 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { background: var(--fill-hover); } +.react-swipeable-view-container { + height: 100%; +}