From a070ed24418fca3aa38a9cc0e99b981645458ad9 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Mon, 4 Dec 2023 10:33:31 +0800 Subject: [PATCH] feat: support all fields type and filters in grid of tauri * feat: support the single select field actions in grid of tauri * feat: support multiselect * feat: support number field and number filter * feat: support url field * fix: eslint error * feat: support checkbox filter * feat: support checklist field * fix: adjusting keydown event * fix: edit record ui * feat: support date field * fix: url field bugs * fix: the bug of the type option wasn't update * chore: make plural tokens compatible with tauri * fix: plural key * fix: optimize get cell performance * fix: update ts error * fix: update select option bugs * fix: grid calculate css * fix: add DidUpdateFieldSettings --------- Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com> --- .../calendar/presentation/calendar_page.dart | 6 +- .../widgets/row/row_property.dart | 12 +- frontend/appflowy_tauri/package.json | 5 +- frontend/appflowy_tauri/pnpm-lock.yaml | 322 +++++++++++++++++- .../appflowy_tauri/scripts/i18n/index.cjs | 14 +- .../src/appflowy_app/assets/arrow-left.svg | 3 + .../src/appflowy_app/assets/arrow-right.svg | 3 + .../components/_shared/ViewTitle/index.tsx | 9 +- .../_shared/app-dialog/ConfirmDialog.tsx | 9 +- .../components/database/Database.hooks.ts | 98 +++++- .../components/database/Database.tsx | 13 +- .../components/database/_shared/CellText.tsx | 2 +- .../database/_shared/VirtualizedList.tsx | 3 +- .../application/cell/cell_listeners.ts | 48 +++ .../database/application/cell/cell_service.ts | 10 + .../database/application/cell/cell_types.ts | 46 ++- .../database/application/cell/index.ts | 1 + .../application/database/database_service.ts | 57 ++-- .../application/database/database_types.ts | 7 +- .../application/field/field_listeners.ts | 33 +- .../application/field/field_service.ts | 29 +- .../database/application/field/field_types.ts | 37 +- .../select_option/select_option_service.ts | 11 +- .../field/type_option/type_option_service.ts | 39 ++- .../field/type_option/type_option_types.ts | 80 +++++ .../application/filter/filter_data.ts | 21 +- .../application/filter/filter_service.ts | 4 +- .../application/filter/filter_types.ts | 126 ++++++- .../database/application/filter/index.ts | 1 + .../database/application/row/row_listeners.ts | 5 +- .../database/components/cell/Cell.hooks.ts | 58 +++- .../database/components/cell/Cell.tsx | 32 +- .../database/components/cell/CheckboxCell.tsx | 18 +- .../components/cell/ChecklistCell.tsx | 55 +++ .../database/components/cell/DateTimeCell.tsx | 55 +++ .../database/components/cell/ExpandButton.tsx | 7 +- .../database/components/cell/NumberCell.tsx | 50 +++ .../database/components/cell/SelectCell.tsx | 76 +++++ .../cell/SelectCell/CreateOption.tsx | 22 -- .../components/cell/SelectCell/SelectCell.tsx | 146 -------- .../cell/SelectCell/SelectOptionItem.tsx | 49 --- .../cell/SelectCell/SelectOptionMenu.tsx | 78 ----- .../components/cell/SelectCell/index.ts | 1 - .../database/components/cell/TextCell.tsx | 79 ++--- .../components/cell/TimestampCell.tsx | 13 + .../database/components/cell/URLCell.tsx | 92 +++++ .../database_settings/DatabaseSettings.tsx | 2 +- .../database_settings/FilterSettings.tsx | 10 +- .../database_settings/Properties.tsx | 7 +- .../database_settings/SortSettings.tsx | 4 + .../components/edit_record/EditRecord.tsx | 7 +- .../record_properties/NewProperty.tsx | 13 +- .../record_properties/Property.tsx | 2 +- .../record_properties/PropertyName.tsx | 4 +- .../record_properties/PropertyValue.tsx | 23 +- .../record_properties/RecordProperties.tsx | 48 ++- .../database/components/field/Field.tsx | 12 +- .../database/components/field/FieldList.tsx | 37 +- .../database/components/field/FieldMenu.tsx | 90 +++-- .../database/components/field/FieldSelect.tsx | 5 +- .../components/field/FieldTypeMenu.tsx | 17 +- .../field/FieldTypeMenuExtension.tsx | 26 ++ .../components/field/FieldTypeSelect.tsx | 57 ++++ .../components/field/FieldTypeText.tsx | 49 +-- .../field_types/checklist/AddNewOption.tsx | 41 +++ .../checklist/ChecklistCellActions.tsx | 41 +++ .../field_types/checklist/ChecklistItem.tsx | 80 +++++ .../checklist/LinearProgressWithLabel.tsx | 17 + .../field_types/date/CustomCalendar.tsx | 87 +++++ .../field_types/date/DateFormat.tsx | 72 ++++ .../field_types/date/DateTimeCellActions.tsx | 153 +++++++++ .../field_types/date/DateTimeFieldActions.tsx | 17 + .../field_types/date/DateTimeFormat.tsx | 75 ++++ .../field_types/date/DateTimeFormatSelect.tsx | 47 +++ .../field_types/date/DateTimeInput.tsx | 81 +++++ .../field_types/date/DateTimeSet.tsx | 54 +++ .../field_types/date/IncludeTimeSwitch.tsx | 30 ++ .../field_types/date/RangeSwitch.tsx | 30 ++ .../field_types/date/TimeFormat.tsx | 67 ++++ .../components/field_types/date/utils.ts | 29 ++ .../number/EditNumberCellInput.tsx | 66 ++++ .../field_types/number/NumberFieldActions.tsx | 34 ++ .../field_types/number/NumberFormatMenu.tsx | 34 ++ .../field_types/number/NumberFormatSelect.tsx | 49 +++ .../components/field_types/number/const.ts | 14 + .../field_types/select/SelectOptionMenu.tsx | 130 +++++++ .../SelectCell => field_types/select}/Tag.tsx | 0 .../select}/constants.ts | 0 .../select_cell_actions/CreateOption.tsx | 16 + .../select_cell_actions/SearchInput.tsx | 41 +++ .../select_cell_actions/SelectCellActions.tsx | 152 +++++++++ .../select_cell_actions/SelectOptionItem.tsx | 57 ++++ .../select_field_actions/AddAnOption.tsx | 54 +++ .../select/select_field_actions/Option.tsx | 46 +++ .../select/select_field_actions/Options.tsx | 19 ++ .../SelectFieldActions.tsx | 26 ++ .../text}/EditTextCellInput.tsx | 28 +- .../components/filter/ConditionSelect.tsx | 40 +++ .../database/components/filter/Filter.tsx | 114 +++++-- .../components/filter/FilterActions.tsx | 1 + .../filter/FilterConditionSelect.tsx | 201 +++++++++++ .../components/filter/FilterFieldsMenu.tsx | 11 +- .../database/components/filter/Filters.tsx | 16 +- .../filter/date_filter/DateFilter.tsx | 98 ++++++ .../filter/field_filter/TextFilter.tsx | 52 --- .../TextFilterConditionSelect.tsx | 54 --- .../filter/select_filter/SelectFilter.tsx | 87 +++++ .../filter/text_filter/TextFilter.tsx | 38 +++ .../components/sort/SortConditionSelect.tsx | 8 +- .../components/sort/SortFieldsMenu.tsx | 7 +- .../database/components/sort/SortItem.tsx | 4 +- .../database/components/sort/SortMenu.tsx | 48 ++- .../database/components/tab_bar/ViewTabs.tsx | 6 +- .../grid/GridCalculate/GridCalculate.tsx | 2 +- .../database/grid/GridField/GridField.tsx | 14 +- .../grid/GridRow/GridCellRow/GridCellRow.tsx | 2 +- .../database/grid/GridTable/GridTable.tsx | 34 +- .../components/layout/Breadcrumb/index.tsx | 4 +- .../appflowy_tauri/src/styles/template.css | 75 ++++ .../style-dictionary/tailwind/colors.cjs | 3 +- frontend/resources/translations/en.json | 33 +- .../src/services/database/database_editor.rs | 2 + 122 files changed, 4094 insertions(+), 845 deletions(-) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/assets/arrow-left.svg create mode 100644 frontend/appflowy_tauri/src/appflowy_app/assets/arrow-right.svg create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/cell_listeners.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/ChecklistCell.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/DateTimeCell.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/NumberCell.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/CreateOption.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectCell.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectOptionItem.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectOptionMenu.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TimestampCell.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/URLCell.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeMenuExtension.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeSelect.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/AddNewOption.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistCellActions.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistItem.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/LinearProgressWithLabel.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/CustomCalendar.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateFormat.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeCellActions.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFieldActions.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormat.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormatSelect.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeInput.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeSet.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/IncludeTimeSwitch.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/RangeSwitch.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/TimeFormat.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/utils.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/EditNumberCellInput.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFieldActions.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatMenu.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatSelect.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/const.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionMenu.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/database/components/{cell/SelectCell => field_types/select}/Tag.tsx (100%) rename frontend/appflowy_tauri/src/appflowy_app/components/database/components/{cell/SelectCell => field_types/select}/constants.ts (100%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/CreateOption.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SearchInput.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectCellActions.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/AddAnOption.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Option.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Options.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/SelectFieldActions.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/database/components/{cell => field_types/text}/EditTextCellInput.tsx (59%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/ConditionSelect.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterConditionSelect.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilter.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/field_filter/TextFilter.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/field_filter/TextFilterConditionSelect.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilter.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilter.tsx diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart index f1a9c5b342..80af1e89ec 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart @@ -422,8 +422,10 @@ class _UnscheduledEventsButtonState extends State { } }, child: FlowyTooltip( - message: LocaleKeys.calendar_settings_noDateHint - .plural(state.unscheduleEvents.length), + message: LocaleKeys.calendar_settings_noDateHint.plural( + state.unscheduleEvents.length, + namedArgs: {'count': '${state.unscheduleEvents.length}'}, + ), child: FlowyText.regular( "${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})", fontSize: 10, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart index 7295643859..9303b76b27 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart @@ -307,10 +307,14 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget { return BlocBuilder( builder: (context, state) { final text = switch (state.showHiddenFields) { - false => LocaleKeys.grid_rowPage_showHiddenFields - .plural(state.numHiddenFields), - true => LocaleKeys.grid_rowPage_hideHiddenFields - .plural(state.numHiddenFields), + false => LocaleKeys.grid_rowPage_showHiddenFields.plural( + state.numHiddenFields, + namedArgs: {'count': '${state.numHiddenFields}'}, + ), + true => LocaleKeys.grid_rowPage_hideHiddenFields.plural( + state.numHiddenFields, + namedArgs: {'count': '${state.numHiddenFields}'}, + ), }; return SizedBox( diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index 53e5bde53a..7612662050 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -25,6 +25,7 @@ "@mui/icons-material": "^5.11.11", "@mui/material": "^5.11.12", "@mui/system": "^5.14.4", + "@mui/x-date-pickers-pro": "^6.18.2", "@reduxjs/toolkit": "^1.9.2", "@slate-yjs/core": "^1.0.0", "@tanstack/react-virtual": "3.0.0-beta.54", @@ -51,6 +52,7 @@ "react-beautiful-dnd": "^13.1.1", "react-calendar": "^4.1.0", "react-color": "^2.19.3", + "react-datepicker": "^4.23.0", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", "react-i18next": "^12.2.0", @@ -66,7 +68,7 @@ "slate-react": "^0.94.2", "ts-results": "^3.3.0", "utf8": "^3.0.0", - "valtio": "^1.11.1", + "valtio": "^1.12.1", "yjs": "^13.5.51" }, "devDependencies": { @@ -83,6 +85,7 @@ "@types/react": "^18.0.15", "@types/react-beautiful-dnd": "^13.1.3", "@types/react-color": "^3.0.6", + "@types/react-datepicker": "^4.19.3", "@types/react-dom": "^18.0.6", "@types/react-katex": "^3.0.0", "@types/react-transition-group": "^4.4.6", diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml index 9c13ac1f54..2829d88eeb 100644 --- a/frontend/appflowy_tauri/pnpm-lock.yaml +++ b/frontend/appflowy_tauri/pnpm-lock.yaml @@ -22,6 +22,9 @@ dependencies: '@mui/system': specifier: ^5.14.4 version: 5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/x-date-pickers-pro': + specifier: ^6.18.2 + version: 6.18.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@mui/material@5.13.0)(@mui/system@5.14.4)(@types/react@18.2.6)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) '@reduxjs/toolkit': specifier: ^1.9.2 version: 1.9.5(react-redux@8.0.5)(react@18.2.0) @@ -100,6 +103,9 @@ dependencies: react-color: specifier: ^2.19.3 version: 2.19.3(react@18.2.0) + react-datepicker: + specifier: ^4.23.0 + version: 4.23.0(react-dom@18.2.0)(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) @@ -146,8 +152,8 @@ dependencies: specifier: ^3.0.0 version: 3.0.0 valtio: - specifier: ^1.11.1 - version: 1.11.1(react@18.2.0) + specifier: ^1.12.1 + version: 1.12.1(@types/react@18.2.6)(react@18.2.0) yjs: specifier: ^13.5.51 version: 13.6.1 @@ -192,6 +198,9 @@ devDependencies: '@types/react-color': specifier: ^3.0.6 version: 3.0.6 + '@types/react-datepicker': + specifier: ^4.19.3 + version: 4.19.3(react-dom@18.2.0)(react@18.2.0) '@types/react-dom': specifier: ^18.0.6 version: 18.2.4 @@ -585,6 +594,12 @@ packages: regenerator-runtime: 0.14.0 dev: false + /@babel/runtime@7.23.4: + resolution: {integrity: sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + /@babel/template@7.20.7: resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} engines: {node: '>=6.9.0'} @@ -993,6 +1008,34 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@floating-ui/core@1.5.0: + resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==} + dependencies: + '@floating-ui/utils': 0.1.6 + dev: false + + /@floating-ui/dom@1.5.3: + resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==} + dependencies: + '@floating-ui/core': 1.5.0 + '@floating-ui/utils': 0.1.6 + dev: false + + /@floating-ui/react-dom@2.0.4(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 1.5.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@floating-ui/utils@0.1.6: + resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==} + dev: false + /@humanwhocodes/config-array@0.11.8: resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} engines: {node: '>=10.10.0'} @@ -1356,6 +1399,29 @@ packages: react-is: 18.2.0 dev: false + /@mui/base@5.0.0-beta.24(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-bKt2pUADHGQtqWDZ8nvL2Lvg2GNJyd/ZUgZAJoYzRgmnxBL9j36MSlS3+exEdYkikcnvVafcBtD904RypFKb0w==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.4 + '@floating-ui/react-dom': 2.0.4(react-dom@18.2.0)(react@18.2.0) + '@mui/types': 7.2.9(@types/react@18.2.6) + '@mui/utils': 5.14.18(@types/react@18.2.6)(react@18.2.0) + '@popperjs/core': 2.11.8 + '@types/react': 18.2.6 + clsx: 2.0.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@mui/core-downloads-tracker@5.13.0: resolution: {integrity: sha512-5nXz2k8Rv2ZjtQY6kXirJVyn2+ODaQuAJmXSJtLDUQDKWp3PFUj6j3bILqR0JGOs9R5ejgwz3crLKsl6GwjwkQ==} dev: false @@ -1493,6 +1559,17 @@ packages: '@types/react': 18.2.6 dev: false + /@mui/types@7.2.9(@types/react@18.2.6): + resolution: {integrity: sha512-k1lN/PolaRZfNsRdAqXtcR71sTnv3z/VCCGPxU8HfdftDkzi335MdJ6scZxvofMAd/K/9EbzCZTFBmlNpQVdCg==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.6 + dev: false + /@mui/utils@5.12.3(react@18.2.0): resolution: {integrity: sha512-D/Z4Ub3MRl7HiUccid7sQYclTr24TqUAQFFlxHQF8FR177BrCTQ0JJZom7EqYjZCdXhwnSkOj2ph685MSKNtIA==} engines: {node: '>=12.0.0'} @@ -1507,6 +1584,24 @@ packages: react-is: 18.2.0 dev: false + /@mui/utils@5.14.18(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-HZDRsJtEZ7WMSnrHV9uwScGze4wM/Y+u6pDVo+grUjt5yXzn+wI8QX/JwTHh9YSw/WpnUL80mJJjgCnWj2VrzQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.4 + '@types/prop-types': 15.7.11 + '@types/react': 18.2.6 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + /@mui/utils@5.14.4(react@18.2.0): resolution: {integrity: sha512-4ANV0txPD3x0IcTCSEHKDWnsutg1K3m6Vz5IckkbLXVYu17oOZCVUdOKsb/txUmaCd0v0PmSRe5PW+Mlvns5dQ==} engines: {node: '>=12.0.0'} @@ -1521,6 +1616,130 @@ packages: react-is: 18.2.0 dev: false + /@mui/x-date-pickers-pro@6.18.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@mui/material@5.13.0)(@mui/system@5.14.4)(@types/react@18.2.6)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-8lEVEOtCQssKWel4Ey1pRulGPXUQ73TnkHKzHWsjdv03FjiUs3eYB+Ej0Uk5yWPmsqlShWhOzOlOGDpzsYJsUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.8.6 + '@mui/system': ^5.8.0 + date-fns: ^2.25.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + dependencies: + '@babel/runtime': 7.23.4 + '@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/base': 5.0.0-beta.24(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.13.0(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/utils': 5.14.18(@types/react@18.2.6)(react@18.2.0) + '@mui/x-date-pickers': 6.18.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@mui/material@5.13.0)(@mui/system@5.14.4)(@types/react@18.2.6)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) + '@mui/x-license-pro': 6.10.2(@types/react@18.2.6)(react@18.2.0) + clsx: 2.0.0 + dayjs: 1.11.9 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + + /@mui/x-date-pickers@6.18.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@mui/material@5.13.0)(@mui/system@5.14.4)(@types/react@18.2.6)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HJq4uoFQSu5isa/mesWw2BKh8KBRYUQb+KaSlVlWfJNgP3YhPvWZ6yqCNYyxOAiPMxb0n3nBjS9ErO27OHjFMA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.8.6 + '@mui/system': ^5.8.0 + date-fns: ^2.25.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + dependencies: + '@babel/runtime': 7.23.4 + '@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/base': 5.0.0-beta.24(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.13.0(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/utils': 5.14.18(@types/react@18.2.6)(react@18.2.0) + '@types/react-transition-group': 4.4.9 + clsx: 2.0.0 + dayjs: 1.11.9 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + + /@mui/x-license-pro@6.10.2(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-Baw3shilU+eHgU+QYKNPFUKvfS5rSyNJ98pQx02E0gKA22hWp/XAt88K1qUfUMPlkPpvg/uci6gviQSSLZkuKw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.23.4 + '@mui/utils': 5.14.18(@types/react@18.2.6)(react@18.2.0) + react: 18.2.0 + transitivePeerDependencies: + - '@types/react' + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1546,6 +1765,9 @@ packages: resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==} dev: false + /@popperjs/core@2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + /@reduxjs/toolkit@1.9.5(react-redux@8.0.5)(react@18.2.0): resolution: {integrity: sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==} peerDependencies: @@ -1998,6 +2220,10 @@ packages: resolution: {integrity: sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==} dev: true + /@types/prop-types@15.7.11: + resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} + dev: false + /@types/prop-types@15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} @@ -2021,6 +2247,18 @@ packages: '@types/reactcss': 1.2.6 dev: true + /@types/react-datepicker@4.19.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-85F9eKWu9fGiD9r4KVVMPYAdkJJswR3Wci9PvqplmB6T+D+VbUqPeKtifg96NZ4nEhufjehW+SX4JLrEWVplWw==} + dependencies: + '@popperjs/core': 2.11.8 + '@types/react': 18.2.6 + date-fns: 2.30.0 + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - react + - react-dom + dev: true + /@types/react-dom@18.2.4: resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==} dependencies: @@ -2064,6 +2302,12 @@ packages: dependencies: '@types/react': 18.2.6 + /@types/react-transition-group@4.4.9: + resolution: {integrity: sha512-ZVNmWumUIh5NhH8aMD9CR2hdW0fNuYInlocZHaZ+dgk/1K49j1w/HoAuK1ki+pgscQrOFRTlXeoURtuzEkV3dg==} + dependencies: + '@types/react': 18.2.6 + dev: false + /@types/react@17.0.59: resolution: {integrity: sha512-gSON5zWYIGyoBcycCE75E9+r6dCC2dHdsrVkOEiIYNU5+Q28HcBAuqvDuxHcCbMfHBHdeT5Tva/AFn3rnMKE4g==} dependencies: @@ -2671,6 +2915,10 @@ packages: /cjs-module-lexer@1.2.2: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} + /classnames@2.3.2: + resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} + dev: false + /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2870,6 +3118,12 @@ packages: whatwg-url: 11.0.0 dev: true + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.23.4 + /dayjs@1.11.9: resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==} dev: false @@ -2923,6 +3177,14 @@ packages: engines: {node: '>=0.4.0'} dev: true + /derive-valtio@0.1.0(valtio@1.12.1): + resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==} + peerDependencies: + valtio: '*' + dependencies: + valtio: 1.12.1(@types/react@18.2.6)(react@18.2.0) + dev: false + /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -5330,6 +5592,22 @@ packages: tinycolor2: 1.6.0 dev: false + /react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==} + peerDependencies: + react: ^16.9.0 || ^17 || ^18 + react-dom: ^16.9.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + classnames: 2.3.2 + date-fns: 2.30.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-onclickoutside: 6.13.0(react-dom@18.2.0)(react@18.2.0) + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) + dev: false + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -5338,7 +5616,6 @@ packages: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.0 - dev: false /react-error-boundary@3.1.4(react@18.2.0): resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} @@ -5361,6 +5638,9 @@ packages: warning: 4.0.3 dev: false + /react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + /react-i18next@12.2.2(i18next@22.4.15)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-KBB6buBmVKXUWNxXHdnthp+38gPyBT46hJCAIQ8rX19NFL/m2ahte2KARfIDf2tMnSAL7wwck6eDOd/9zn6aFg==} peerDependencies: @@ -5402,6 +5682,29 @@ packages: react: 18.2.0 dev: false + /react-onclickoutside@6.13.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==} + peerDependencies: + react: ^15.5.x || ^16.x || ^17.x || ^18.x + react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-fast-compare: 3.2.2 + warning: 4.0.3 + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} peerDependencies: @@ -5551,7 +5854,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: false /reactcss@1.2.3(react@18.2.0): resolution: {integrity: sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==} @@ -5599,7 +5901,6 @@ packages: /regenerator-runtime@0.14.0: resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} - dev: false /regexp.prototype.flags@1.5.0: resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} @@ -5718,7 +6019,6 @@ packages: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: loose-envify: 1.4.0 - dev: false /scroll-into-view-if-needed@2.2.31: resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==} @@ -6375,15 +6675,20 @@ packages: '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 - /valtio@1.11.1(react@18.2.0): - resolution: {integrity: sha512-sTKWY1e1AVUu4sY9CimoSZpufAsAXO+fzZrw0X5xtijEmDDQaPPLHZxlpONUpTLtvxPjpQURCSdUuUyBszoEOg==} + /valtio@1.12.1(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-R0V4H86Xi2Pp7pmxN/EtV4Q6jr6PMN3t1IwxEvKUp6160r8FimvPh941oWyeK1iec/DTsh9Jb3Q+GputMS8SYg==} engines: {node: '>=12.20.0'} peerDependencies: + '@types/react': '>=16.8' react: '>=16.8' peerDependenciesMeta: + '@types/react': + optional: true react: optional: true dependencies: + '@types/react': 18.2.6 + derive-valtio: 0.1.0(valtio@1.12.1) proxy-compare: 2.5.1 react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) @@ -6457,7 +6762,6 @@ packages: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} dependencies: loose-envify: 1.4.0 - dev: false /webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} diff --git a/frontend/appflowy_tauri/scripts/i18n/index.cjs b/frontend/appflowy_tauri/scripts/i18n/index.cjs index 4deae757fb..c3789e0c56 100644 --- a/frontend/appflowy_tauri/scripts/i18n/index.cjs +++ b/frontend/appflowy_tauri/scripts/i18n/index.cjs @@ -36,17 +36,25 @@ languages.forEach(language => { console.error(res); } }) -}) +}); + + function flattenJSON(obj, prefix = '') { let result = {}; + const pluralsKey = ["one", "other", "few", "many", "two", "zero"]; for (let key in obj) { if (typeof obj[key] === 'object' && obj[key] !== null) { + const nestedKeys = flattenJSON(obj[key], `${prefix}${key}.`); result = { ...result, ...nestedKeys }; } else { - - result[`${prefix}${key}`] = obj[key].replaceAll('{', '{{').replaceAll('}', '}}'); + let newKey = `${prefix}${key}`; + let replaceChar = '{' + if (pluralsKey.includes(key)) { + newKey = `${prefix.slice(0, -1)}_${key}`; + } + result[newKey] = obj[key].replaceAll('{', '{{').replaceAll('}', '}}'); } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/arrow-left.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/arrow-left.svg new file mode 100644 index 0000000000..e4ab9068be --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/arrow-right.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/arrow-right.svg new file mode 100644 index 0000000000..dc40ae52a6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/ViewTitle/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/ViewTitle/index.tsx index 55373e6414..7ad0fb86e7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/ViewTitle/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/ViewTitle/index.tsx @@ -1,4 +1,4 @@ -import React, { FormEventHandler, useCallback, useEffect, useRef, useState } from 'react'; +import React, { FormEventHandler, useCallback, useRef, useState } from 'react'; import ViewBanner from '$app/components/_shared/ViewTitle/ViewBanner'; import { Page, PageIcon } from '$app_reducers/pages/slice'; import { ViewIconTypePB } from '@/services/backend'; @@ -37,13 +37,6 @@ function ViewTitle({ view, onTitleChange: onTitleChangeProp, onUpdateIcon: onUpd [onUpdateIconProp] ); - useEffect(() => { - if (textareaRef.current) { - textareaRef.current.focus(); - // set the cursor to the end of the text - textareaRef.current.setSelectionRange(textareaRef.current.value.length, textareaRef.current.value.length); - } - }, []); return (
setHover(true)} onMouseLeave={() => setHover(false)}> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/app-dialog/ConfirmDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/app-dialog/ConfirmDialog.tsx index d1d1636664..e506a74990 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/app-dialog/ConfirmDialog.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/app-dialog/ConfirmDialog.tsx @@ -1,6 +1,6 @@ import React from 'react'; import DialogContent from '@mui/material/DialogContent'; -import { Button, DialogActions } from '@mui/material'; +import { Button, DialogActions, Divider } from '@mui/material'; import Dialog from '@mui/material/Dialog'; import { useTranslation } from 'react-i18next'; @@ -18,10 +18,11 @@ function ConfirmDialog({ open, title, subtitle, onOk, onClose }: Props) { return ( e.stopPropagation()} open={open} onClose={onClose}> -
{title}
-
{subtitle}
+
{title}
+ {subtitle &&
{subtitle}
}
- + + 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 2ea447d09e..d742715fa3 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,11 +1,19 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { proxy, useSnapshot } from 'valtio'; + import { DatabaseLayoutPB, DatabaseNotification, FieldVisibility } from '@/services/backend'; import { subscribeNotifications } from '$app/hooks'; -import { Database, databaseService, fieldListeners, fieldService, rowListeners, sortListeners } from './application'; -import { didUpdateFilter } from '$app/components/database/application/filter/filter_listeners'; -import { didUpdateViewRowsVisibility } from '$app/components/database/application/row/row_listeners'; +import { + Cell, + Database, + databaseService, + cellListeners, + fieldListeners, + rowListeners, + sortListeners, + filterListeners, +} from './application'; export function useSelectDatabaseView({ viewId }: { viewId?: string }) { const key = 'v'; @@ -36,16 +44,73 @@ const DatabaseContext = createContext({ sorts: [], groupSettings: [], groups: [], + typeOptions: {}, + cells: {}, }); export const DatabaseProvider = DatabaseContext.Provider; export const useDatabase = () => useSnapshot(useContext(DatabaseContext)); +export const useSelectorCell = (rowId: string, fieldId: string) => { + const database = useContext(DatabaseContext); + const cells = useSnapshot(database.cells); + + return cells[`${rowId}:${fieldId}`]; +}; + +export const useDispatchCell = () => { + const database = useContext(DatabaseContext); + + const setCell = useCallback( + (cell: Cell) => { + const id = `${cell.rowId}:${cell.fieldId}`; + + database.cells[id] = cell; + }, + [database] + ); + + const deleteCells = useCallback( + ({ rowId, fieldId }: { rowId: string; fieldId?: string }) => { + cellListeners.didDeleteCells({ database, rowId, fieldId }); + }, + [database] + ); + + return { + deleteCells, + setCell, + }; +}; + +export const useTypeOptions = () => { + const context = useContext(DatabaseContext); + + return useSnapshot(context.typeOptions); +}; + +export const useFiltersCount = () => { + const { filters, fields } = useDatabase(); + + // filter fields: if the field is deleted, it will not be displayed + return useMemo( + () => filters?.map((filter) => fields.find((field) => field.id === filter.fieldId)).filter(Boolean).length, + [filters, fields] + ); +}; + +export function useTypeOption(fieldId: string) { + const context = useContext(DatabaseContext); + const typeOptions = useSnapshot(context.typeOptions); + + return typeOptions[fieldId] as T; +} + export const useDatabaseVisibilityRows = () => { const { rowMetas } = useDatabase(); - return useMemo(() => rowMetas.filter((row) => !row.isHidden), [rowMetas]); + return useMemo(() => rowMetas.filter((row) => row && !row.isHidden), [rowMetas]); }; export const useDatabaseVisibilityFields = () => { @@ -69,6 +134,8 @@ export const useConnectDatabase = (viewId: string) => { sorts: [], groupSettings: [], groups: [], + typeOptions: {}, + cells: {}, }); void databaseService.openDatabase(viewId).then((value) => Object.assign(proxyDatabase, value)); @@ -79,10 +146,10 @@ export const useConnectDatabase = (viewId: string) => { useEffect(() => { const unsubscribePromise = subscribeNotifications( { - [DatabaseNotification.DidUpdateFields]: async () => { - database.fields = await fieldService.getFields(viewId); + [DatabaseNotification.DidUpdateFields]: async (changeset) => { + await fieldListeners.didUpdateFields(viewId, database, changeset); }, - [DatabaseNotification.DidUpdateFieldSettings]: async (changeset) => { + [DatabaseNotification.DidUpdateFieldSettings]: (changeset) => { fieldListeners.didUpdateFieldSettings(database, changeset); }, [DatabaseNotification.DidUpdateViewRows]: (changeset) => { @@ -100,10 +167,10 @@ export const useConnectDatabase = (viewId: string) => { }, [DatabaseNotification.DidUpdateFilter]: (changeset) => { - didUpdateFilter(database, changeset); + filterListeners.didUpdateFilter(database, changeset); }, [DatabaseNotification.DidUpdateViewRowsVisibility]: (changeset) => { - didUpdateViewRowsVisibility(database, changeset); + rowListeners.didUpdateViewRowsVisibility(database, changeset); }, }, { id: viewId } @@ -115,9 +182,10 @@ export const useConnectDatabase = (viewId: string) => { return database; }; -export function useDatabaseResize() { +export function useDatabaseResize(selectedViewId?: string) { const ref = useRef(null); const collectionRef = useRef(null); + const [openCollections, setOpenCollections] = useState([]); const [tableHeight, setTableHeight] = useState(0); @@ -140,23 +208,23 @@ export function useDatabaseResize() { }; handleResize(); - const resizeObserver = new ResizeObserver(() => { - handleResize(); - }); + const resizeObserver = new ResizeObserver(handleResize); resizeObserver.observe(element); if (collectionElement) { - resizeObserver.observe(collectionRef.current); + 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 b9aada517d..f827bb62b9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx @@ -24,8 +24,7 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => { const { t } = useTranslation(); const [notFound, setNotFound] = useState(false); const [childViewIds, setChildViewIds] = useState([]); - const { ref, collectionRef, tableHeight } = useDatabaseResize(); - const [openCollections, setOpenCollections] = useState([]); + const { ref, collectionRef, tableHeight, openCollections, setOpenCollections } = useDatabaseResize(selectedViewId); useEffect(() => { const onPageChanged = () => { @@ -54,7 +53,7 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => { }; }, [viewId]); - const index = useMemo(() => { + const value = useMemo(() => { return Math.max(0, childViewIds.indexOf(selectedViewId ?? viewId)); }, [childViewIds, selectedViewId, viewId]); @@ -77,7 +76,7 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => { setOpenCollections((prev) => [...prev, id]); } }, - [openCollections] + [openCollections, setOpenCollections] ); if (notFound) { @@ -102,10 +101,10 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => { }} className={'flex-1 overflow-hidden'} axis={'x'} - index={index} + index={value} > - {childViewIds.map((id) => ( - + {childViewIds.map((id, index) => ( + {selectedViewId === id && ( <> 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 75ef8d838a..f9d03c2838 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,7 +9,7 @@ export const CellText = React.forwardRef +
{children}
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/VirtualizedList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/VirtualizedList.tsx index 1ce7ffea21..c27bcc43c5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/VirtualizedList.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/VirtualizedList.tsx @@ -33,10 +33,11 @@ export const VirtualizedList: FC = ({ return (
{ + const cell = database.cells[id]; + + if (rowId && cell.rowId !== rowId) return; + if (fieldId && cell.fieldId !== fieldId) return; + + delete database.cells[id]; + }); +} + +export async function didUpdateCells({ + viewId, + database, + rowId, + fieldId, +}: { + viewId: string; + database: Database; + rowId?: string; + fieldId?: string; +}) { + const field = database.fields.find((field) => field.id === fieldId); + + if (!field) { + delete database.cells[`${rowId}:${fieldId}`]; + return; + } + + const ids = Object.keys(database.cells); + + ids.forEach((id) => { + const cell = database.cells[id]; + + if (rowId && cell.rowId !== rowId) return; + if (fieldId && cell.fieldId !== fieldId) return; + + void getCell(viewId, cell.rowId, cell.fieldId, field.type).then((data) => { + // cache cell + database.cells[id] = data; + }); + }); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/cell_service.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/cell_service.ts index 3054e7f761..16cd8a1b64 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/cell_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/cell_service.ts @@ -102,10 +102,17 @@ export async function updateDateCell( rowId: string, fieldId: string, data: { + // 10-digit timestamp date?: number; + // time string in format HH:mm time?: string; + // 10-digit timestamp + endDate?: number; + // time string in format HH:mm + endTime?: string; includeTime?: boolean; clearFlag?: boolean; + isRange?: boolean; } ): Promise { const payload = DateChangesetPB.fromObject({ @@ -118,6 +125,9 @@ export async function updateDateCell( time: data.time, include_time: data.includeTime, clear_flag: data.clearFlag, + end_date: data.endDate, + end_time: data.endTime, + is_range: data.isRange, }); const result = await DatabaseEventUpdateDateCell(payload); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/cell_types.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/cell_types.ts index 5dfeda618c..b8cc81b20b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/cell_types.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/cell_types.ts @@ -4,8 +4,13 @@ import { DateCellDataPB, FieldType, SelectOptionCellDataPB, + TimestampCellDataPB, URLCellDataPB, } from '@/services/backend'; +import { + SelectOption, + pbToSelectOption, +} from '$app/components/database/application/field/select_option/select_option_types'; export interface Cell { rowId: string; @@ -53,11 +58,25 @@ export interface DateTimeCell extends Cell { data: DateTimeCellData; } +export interface TimeStampCell extends Cell { + fieldType: FieldType.LastEditedTime | FieldType.CreatedTime; + data: TimestampCellData; +} + export interface DateTimeCellData { date?: string; time?: string; timestamp?: number; includeTime?: boolean; + endDate?: string; + endTime?: string; + endTimestamp?: number; + isRange?: boolean; +} + +export interface TimestampCellData { + dataTime?: string; + timestamp?: number; } export interface ChecklistCell extends Cell { @@ -71,20 +90,37 @@ export interface ChecklistCellData { */ selectedOptions?: string[]; percentage?: number; + options?: SelectOption[]; } -export type UndeterminedCell = TextCell | NumberCell | DateTimeCell | SelectCell | CheckboxCell | UrlCell | ChecklistCell; +export type UndeterminedCell = + | TextCell + | NumberCell + | DateTimeCell + | SelectCell + | CheckboxCell + | UrlCell + | ChecklistCell; -const pbToDateCellData = (pb: DateCellDataPB): DateTimeCellData => ({ +const pbToDateTimeCellData = (pb: DateCellDataPB): DateTimeCellData => ({ date: pb.date, time: pb.time, timestamp: pb.timestamp, includeTime: pb.include_time, + endDate: pb.end_date, + endTime: pb.end_time, + endTimestamp: pb.end_timestamp, + isRange: pb.is_range, +}); + +const pbToTimestampCellData = (pb: TimestampCellDataPB): TimestampCellData => ({ + dataTime: pb.date_time, + timestamp: pb.timestamp, }); export const pbToSelectCellData = (pb: SelectOptionCellDataPB): SelectCellData => { return { - selectedOptionIds: pb.select_options.map(option => option.id), + selectedOptionIds: pb.select_options.map((option) => option.id), }; }; @@ -96,6 +132,7 @@ const pbToURLCellData = (pb: URLCellDataPB): UrlCellData => ({ export const pbToChecklistCellData = (pb: ChecklistCellDataPB): ChecklistCellData => ({ selectedOptions: pb.selected_options.map(({ id }) => id), percentage: pb.percentage, + options: pb.options.map(pbToSelectOption), }); function bytesToCellData(bytes: Uint8Array, fieldType: FieldType) { @@ -105,9 +142,10 @@ function bytesToCellData(bytes: Uint8Array, fieldType: FieldType) { case FieldType.Checkbox: return new TextDecoder().decode(bytes); case FieldType.DateTime: + return pbToDateTimeCellData(DateCellDataPB.deserialize(bytes)); case FieldType.LastEditedTime: case FieldType.CreatedTime: - return pbToDateCellData(DateCellDataPB.deserialize(bytes)); + return pbToTimestampCellData(TimestampCellDataPB.deserialize(bytes)); case FieldType.SingleSelect: case FieldType.MultiSelect: return pbToSelectCellData(SelectOptionCellDataPB.deserialize(bytes)); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/index.ts index f15008d47c..bc6bdc4417 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/index.ts @@ -1,2 +1,3 @@ export * from './cell_types'; export * as cellService from './cell_service'; +export * as cellListeners from './cell_listeners'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database/database_service.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database/database_service.ts index ca61a966bd..559ed32b7a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database/database_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database/database_service.ts @@ -16,10 +16,9 @@ export async function getDatabaseId(viewId: string): Promise { const result = await DatabaseEventGetDatabaseId(payload); - return result.map(value => value.value).unwrap(); + return result.map((value) => value.value).unwrap(); } - export async function getDatabase(viewId: string) { const payload = DatabaseViewIdPB.fromObject({ value: viewId, @@ -27,15 +26,17 @@ export async function getDatabase(viewId: string) { const result = await DatabaseEventGetDatabase(payload); - return result.map(value => { - return { - id: value.id, - isLinked: value.is_linked, - layoutType: value.layout_type, - fieldIds: value.fields.map(field => field.field_id), - rowMetas: value.rows.map(pbToRowMeta), - }; - }).unwrap(); + return result + .map((value) => { + return { + id: value.id, + isLinked: value.is_linked, + layoutType: value.layout_type, + fieldIds: value.fields.map((field) => field.field_id), + rowMetas: value.rows.map(pbToRowMeta), + }; + }) + .unwrap(); } export async function getDatabaseSetting(viewId: string) { @@ -45,31 +46,23 @@ export async function getDatabaseSetting(viewId: string) { const result = await DatabaseEventGetDatabaseSetting(payload); - return result.map(value => { - return { - filters: value.filters.items.map(pbToFilter), - sorts: value.sorts.items.map(pbToSort), - groupSettings: value.group_settings.items.map(pbToGroupSetting), - }; - }).unwrap(); + return result + .map((value) => { + return { + filters: value.filters.items.map(pbToFilter), + sorts: value.sorts.items.map(pbToSort), + groupSettings: value.group_settings.items.map(pbToGroupSetting), + }; + }) + .unwrap(); } export async function openDatabase(viewId: string): Promise { - const { - id, - isLinked, - layoutType, - fieldIds, - rowMetas, - } = await getDatabase(viewId); + const { id, isLinked, layoutType, fieldIds, rowMetas } = await getDatabase(viewId); - const { - filters, - sorts, - groupSettings, - } = await getDatabaseSetting(viewId); + const { filters, sorts, groupSettings } = await getDatabaseSetting(viewId); - const fields = await fieldService.getFields(viewId, fieldIds); + const { fields, typeOptions } = await fieldService.getFields(viewId, fieldIds); const groups = await groupService.getGroups(viewId); @@ -83,5 +76,7 @@ export async function openDatabase(viewId: string): Promise { sorts, groups, groupSettings, + typeOptions, + cells: {}, }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database/database_types.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database/database_types.ts index 2265421687..627cd94013 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database/database_types.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database/database_types.ts @@ -1,18 +1,21 @@ import { DatabaseLayoutPB } from '@/services/backend'; -import { Field } from '../field'; +import { Field, UndeterminedTypeOptionData } from '../field'; import { Filter } from '../filter'; import { GroupSetting, Group } from '../group'; import { RowMeta } from '../row'; import { Sort } from '../sort'; +import { Cell } from '../cell'; export interface Database { id: string; isLinked: boolean; - layoutType: DatabaseLayoutPB, + layoutType: DatabaseLayoutPB; fields: Field[]; rowMetas: RowMeta[]; filters: Filter[]; sorts: Sort[]; groupSettings: GroupSetting[]; groups: Group[]; + typeOptions: Record; + cells: Record; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_listeners.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_listeners.ts index b000ce5edc..426cfd6d48 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_listeners.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_listeners.ts @@ -1,5 +1,6 @@ -import { FieldSettingsPB } from '@/services/backend'; -import { Database } from '$app/components/database/application'; +import { DatabaseFieldChangesetPB, FieldSettingsPB, FieldVisibility } from '@/services/backend'; +import { Database, fieldService } from '$app/components/database/application'; +import { didDeleteCells, didUpdateCells } from '$app/components/database/application/cell/cell_listeners'; export function didUpdateFieldSettings(database: Database, settings: FieldSettingsPB) { const { field_id: fieldId, visibility, width } = settings; @@ -8,4 +9,32 @@ export function didUpdateFieldSettings(database: Database, settings: FieldSettin if (!field) return; field.visibility = visibility; field.width = width; + // delete cells if field is hidden + if (visibility === FieldVisibility.AlwaysHidden) { + didDeleteCells({ database, fieldId }); + } +} + +export async function didUpdateFields(viewId: string, database: Database, changeset: DatabaseFieldChangesetPB) { + const { fields, typeOptions } = await fieldService.getFields(viewId); + + database.fields = fields; + const deletedFieldIds = Object.keys(changeset.deleted_fields); + const updatedFieldIds = changeset.updated_fields.map((field) => field.id); + + Object.assign(database.typeOptions, typeOptions); + deletedFieldIds.forEach( + (fieldId) => { + // delete cache cells + didDeleteCells({ database, fieldId }); + // delete cache type options + delete database.typeOptions[fieldId]; + }, + [database.typeOptions] + ); + + updatedFieldIds.forEach((fieldId) => { + // delete cache cells + void didUpdateCells({ viewId, database, fieldId }); + }); } 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 35adc72ad1..53bc56a6ec 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 @@ -24,9 +24,16 @@ import { DatabaseEventGetAllFieldSettings, } from '@/services/backend/events/flowy-database2'; import { Field, pbToField } from './field_types'; -import { bytesToTypeOption, getTypeOption } from './type_option'; +import { getTypeOption } from './type_option'; +import { Database } from '$app/components/database/application'; -export async function getFields(viewId: string, fieldIds?: string[]): Promise { +export async function getFields( + viewId: string, + fieldIds?: string[] +): Promise<{ + fields: Field[]; + typeOptions: Database['typeOptions']; +}> { const payload = GetFieldPayloadPB.fromObject({ view_id: viewId, field_ids: fieldIds @@ -48,22 +55,29 @@ export async function getFields(viewId: string, fieldIds?: string[]): Promise { const setting = settings.val.items.find((setting) => setting.field_id === item.id); + const field = pbToField(item); - const typeOption = await getTypeOption(viewId, field.id, field.type); + + const typeOption = await getTypeOption(viewId, item.id, item.field_type); + + if (typeOption) { + typeOptions[item.id] = typeOption; + } return { ...field, visibility: setting?.visibility, width: setting?.width, - typeOption, }; }) ); - return fields; + return { fields, typeOptions }; } export async function createField(viewId: string, fieldType?: FieldType, data?: Uint8Array): Promise { @@ -79,10 +93,7 @@ export async function createField(viewId: string, fieldType?: FieldType, data?: return Promise.reject('Failed to create field'); } - const field = pbToField(result.val.field); - - field.typeOption = bytesToTypeOption(result.val.type_option_data, field.type); - return field; + return pbToField(result.val.field); } export async function duplicateField(viewId: string, fieldId: string): Promise { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_types.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_types.ts index 5c6a4f01fd..00e7e02d4e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_types.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_types.ts @@ -1,11 +1,9 @@ import { FieldPB, FieldType, FieldVisibility } from '@/services/backend'; -import { DateTimeTypeOption, NumberTypeOption, SelectTypeOption } from './type_option/type_option_types'; export interface Field { id: string; name: string; type: FieldType; - typeOption?: unknown; visibility?: FieldVisibility; width?: number; isPrimary: boolean; @@ -13,24 +11,41 @@ export interface Field { export interface NumberField extends Field { type: FieldType.Number; - typeOption: NumberTypeOption; } export interface DateTimeField extends Field { type: FieldType.DateTime; - typeOption: DateTimeTypeOption; } +export interface LastEditedTimeField extends Field { + type: FieldType.LastEditedTime; +} + +export interface CreatedTimeField extends Field { + type: FieldType.CreatedTime; +} + +export type UndeterminedDateField = DateTimeField | CreatedTimeField | LastEditedTimeField; + export interface SelectField extends Field { type: FieldType.SingleSelect | FieldType.MultiSelect; - typeOption: SelectTypeOption; +} + +export interface ChecklistField extends Field { + type: FieldType.Checklist; +} + +export interface DateTimeField extends Field { + type: FieldType.DateTime; } export type UndeterminedField = NumberField | DateTimeField | SelectField | Field; -export const pbToField = (pb: FieldPB): Field => ({ - id: pb.id, - name: pb.name, - type: pb.field_type, - isPrimary: pb.is_primary, -}); +export const pbToField = (pb: FieldPB): Field => { + return { + id: pb.id, + name: pb.name, + type: pb.field_type, + isPrimary: pb.is_primary, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/select_option/select_option_service.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/select_option/select_option_service.ts index 0e32c94ffd..5757b8185d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/select_option/select_option_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/select_option/select_option_service.ts @@ -1,7 +1,4 @@ -import { - CreateSelectOptionPayloadPB, - RepeatedSelectOptionPayload, -} from '@/services/backend'; +import { CreateSelectOptionPayloadPB, RepeatedSelectOptionPayload } from '@/services/backend'; import { DatabaseEventCreateSelectOption, DatabaseEventInsertOrUpdateSelectOption, @@ -28,7 +25,7 @@ export async function insertOrUpdateSelectOption( viewId: string, fieldId: string, items: Partial[], - rowId?: string, + rowId?: string ): Promise { const payload = RepeatedSelectOptionPayload.fromObject({ view_id: viewId, @@ -46,13 +43,13 @@ export async function deleteSelectOption( viewId: string, fieldId: string, items: Partial[], - rowId?: string, + rowId?: string ): Promise { const payload = RepeatedSelectOptionPayload.fromObject({ view_id: viewId, field_id: fieldId, row_id: rowId, - items: items, + items, }); const result = await DatabaseEventDeleteSelectOption(payload); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/type_option/type_option_service.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/type_option/type_option_service.ts index 046eeb5139..89997bd0c9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/type_option/type_option_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/type_option/type_option_service.ts @@ -1,9 +1,9 @@ +import { FieldType, TypeOptionPathPB, TypeOptionChangesetPB } from '@/services/backend'; import { - FieldType, - TypeOptionPathPB, -} from '@/services/backend'; -import { DatabaseEventGetTypeOption } from '@/services/backend/events/flowy-database2'; -import { bytesToTypeOption } from './type_option_types'; + DatabaseEventGetTypeOption, + DatabaseEventUpdateFieldTypeOption, +} from '@/services/backend/events/flowy-database2'; +import { bytesToTypeOption, UndeterminedTypeOptionData, typeOptionDataToPB } from './type_option_types'; export async function getTypeOption(viewId: string, fieldId: string, fieldType: FieldType) { const payload = TypeOptionPathPB.fromObject({ @@ -14,5 +14,32 @@ export async function getTypeOption(viewId: string, fieldId: string, fieldType: const result = await DatabaseEventGetTypeOption(payload); - return result.map(value => bytesToTypeOption(value.type_option_data, fieldType)).unwrap(); + if (!result.ok) { + return Promise.reject(result.val); + } + + const value = result.val; + + return bytesToTypeOption(value.type_option_data, fieldType); +} + +export async function updateTypeOption( + viewId: string, + fieldId: string, + fieldType: FieldType, + data: UndeterminedTypeOptionData +) { + const payload = TypeOptionChangesetPB.fromObject({ + view_id: viewId, + field_id: fieldId, + type_option_data: typeOptionDataToPB(data, fieldType)?.serialize(), + }); + + const result = await DatabaseEventUpdateFieldTypeOption(payload); + + if (!result.ok) { + return Promise.reject(result.val); + } + + return; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/type_option/type_option_types.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/type_option/type_option_types.ts index cc291bd1e2..c72ba886bb 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/type_option/type_option_types.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/type_option/type_option_types.ts @@ -8,6 +8,9 @@ import { RichTextTypeOptionPB, SingleSelectTypeOptionPB, TimeFormatPB, + ChecklistTypeOptionPB, + DateTypeOptionPB, + TimestampTypeOptionPB, } from '@/services/backend'; import { pbToSelectOption, SelectOption } from '../select_option'; @@ -26,6 +29,9 @@ export interface DateTimeTypeOption { dateFormat?: DateFormatPB; timeFormat?: TimeFormatPB; timezoneId?: string; +} +export interface TimeStampTypeOption extends DateTimeTypeOption { + includeTime?: boolean; fieldType?: FieldType; } @@ -38,6 +44,50 @@ export interface CheckboxTypeOption { isSelected?: boolean; } +export interface ChecklistTypeOption { + config?: string; +} + +export type UndeterminedTypeOptionData = + | TextTypeOption + | NumberTypeOption + | SelectTypeOption + | CheckboxTypeOption + | ChecklistTypeOption + | DateTimeTypeOption + | TimeStampTypeOption; + +export function typeOptionDataToPB(data: UndeterminedTypeOptionData, fieldType: FieldType) { + switch (fieldType) { + case FieldType.Number: + return NumberTypeOptionPB.fromObject(data as NumberTypeOption); + case FieldType.DateTime: + return dateTimeTypeOptionToPB(data as DateTimeTypeOption); + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return timestampTypeOptionToPB(data as TimeStampTypeOption); + + default: + return null; + } +} + +function dateTimeTypeOptionToPB(data: DateTimeTypeOption): DateTypeOptionPB { + return DateTypeOptionPB.fromObject({ + time_format: data.timeFormat, + date_format: data.dateFormat, + timezone_id: data.timezoneId, + }); +} + +function timestampTypeOptionToPB(data: TimeStampTypeOption): TimestampTypeOptionPB { + return TimestampTypeOptionPB.fromObject({ + include_time: data.includeTime, + date_format: data.dateFormat, + time_format: data.timeFormat, + field_type: data.fieldType, + }); +} function pbToSelectTypeOption(pb: SingleSelectTypeOptionPB | MultiSelectTypeOptionPB): SelectTypeOption { return { @@ -52,6 +102,29 @@ function pbToCheckboxTypeOption(pb: CheckboxTypeOptionPB): CheckboxTypeOption { }; } +function pbToChecklistTypeOption(pb: ChecklistTypeOptionPB): ChecklistTypeOption { + return { + config: pb.config, + }; +} + +function pbToDateTypeOption(pb: DateTypeOptionPB): DateTimeTypeOption { + return { + dateFormat: pb.date_format, + timezoneId: pb.timezone_id, + timeFormat: pb.time_format, + }; +} + +function pbToTimeStampTypeOption(pb: TimestampTypeOptionPB): TimeStampTypeOption { + return { + includeTime: pb.include_time, + dateFormat: pb.date_format, + timeFormat: pb.time_format, + fieldType: pb.field_type, + }; +} + export function bytesToTypeOption(data: Uint8Array, fieldType: FieldType) { switch (fieldType) { case FieldType.RichText: @@ -64,5 +137,12 @@ export function bytesToTypeOption(data: Uint8Array, fieldType: FieldType) { return pbToSelectTypeOption(MultiSelectTypeOptionPB.deserialize(data)); case FieldType.Checkbox: return pbToCheckboxTypeOption(CheckboxTypeOptionPB.deserialize(data)); + case FieldType.Checklist: + return pbToChecklistTypeOption(ChecklistTypeOptionPB.deserialize(data)); + case FieldType.DateTime: + return pbToDateTypeOption(DateTypeOptionPB.deserialize(data)); + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return pbToTimeStampTypeOption(TimestampTypeOptionPB.deserialize(data)); } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_data.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_data.ts index 847b1139ab..572d8187f1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_data.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_data.ts @@ -1,13 +1,32 @@ -import { TextFilterConditionPB, FieldType } from '@/services/backend'; +import { + CheckboxFilterConditionPB, + ChecklistFilterConditionPB, + FieldType, + NumberFilterConditionPB, + TextFilterConditionPB, +} from '@/services/backend'; import { UndeterminedFilter } from '$app/components/database/application'; export function getDefaultFilter(fieldType: FieldType): UndeterminedFilter['data'] | undefined { switch (fieldType) { case FieldType.RichText: + case FieldType.URL: return { condition: TextFilterConditionPB.Contains, content: '', }; + case FieldType.Number: + return { + condition: NumberFilterConditionPB.NumberIsNotEmpty, + }; + case FieldType.Checkbox: + return { + condition: CheckboxFilterConditionPB.IsUnChecked, + }; + case FieldType.Checklist: + return { + condition: ChecklistFilterConditionPB.IsIncomplete, + }; default: return; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_service.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_service.ts index 64a6ed9c8c..97fcb6e505 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_service.ts @@ -26,7 +26,7 @@ export async function insertFilter({ viewId: string; fieldId: string; fieldType: FieldType; - data: UndeterminedFilter['data']; + data?: UndeterminedFilter['data']; filterId?: string; }): Promise { const payload = DatabaseSettingChangesetPB.fromObject({ @@ -36,7 +36,7 @@ export async function insertFilter({ field_id: fieldId, field_type: fieldType, filter_id: filterId, - data: filterDataToPB(data, fieldType)?.serialize(), + data: data ? filterDataToPB(data, fieldType)?.serialize() : undefined, }, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_types.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_types.ts index 1dcab6eef5..9e6f9f87ce 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_types.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_types.ts @@ -1,10 +1,18 @@ import { + CheckboxFilterConditionPB, + CheckboxFilterPB, FieldType, - TextFilterConditionPB, - SelectOptionConditionPB, - TextFilterPB, - SelectOptionFilterPB, FilterPB, + NumberFilterConditionPB, + NumberFilterPB, + SelectOptionConditionPB, + SelectOptionFilterPB, + TextFilterConditionPB, + TextFilterPB, + ChecklistFilterConditionPB, + ChecklistFilterPB, + DateFilterConditionPB, + DateFilterPB, } from '@/services/backend'; export interface Filter { @@ -29,16 +37,63 @@ export interface SelectFilter extends Filter { data: SelectFilterData; } +export interface NumberFilter extends Filter { + fieldType: FieldType.Number; + data: NumberFilterData; +} + +export interface CheckboxFilter extends Filter { + fieldType: FieldType.Checkbox; + data: CheckboxFilterData; +} + +export interface CheckboxFilterData { + condition?: CheckboxFilterConditionPB; +} + +export interface ChecklistFilter extends Filter { + fieldType: FieldType.Checklist; + data: ChecklistFilterData; +} + +export interface DateFilter extends Filter { + fieldType: FieldType.DateTime | FieldType.CreatedTime | FieldType.LastEditedTime; + data: DateFilterData; +} + +export interface ChecklistFilterData { + condition?: ChecklistFilterConditionPB; +} + export interface SelectFilterData { condition?: SelectOptionConditionPB; optionIds?: string[]; } -export type UndeterminedFilter = TextFilter | SelectFilter; +export interface NumberFilterData { + condition: NumberFilterConditionPB; + content?: string; +} + +export interface DateFilterData { + condition: DateFilterConditionPB; + start?: number; + end?: number; + timestamp?: number; +} + +export type UndeterminedFilter = + | TextFilter + | SelectFilter + | NumberFilter + | CheckboxFilter + | ChecklistFilter + | DateFilter; export function filterDataToPB(data: UndeterminedFilter['data'], fieldType: FieldType) { switch (fieldType) { case FieldType.RichText: + case FieldType.URL: return TextFilterPB.fromObject({ condition: (data as TextFilterData).condition, content: (data as TextFilterData).content, @@ -49,6 +104,28 @@ export function filterDataToPB(data: UndeterminedFilter['data'], fieldType: Fiel condition: (data as SelectFilterData).condition, option_ids: (data as SelectFilterData).optionIds, }); + case FieldType.Number: + return NumberFilterPB.fromObject({ + condition: (data as NumberFilterData).condition, + content: (data as NumberFilterData).content, + }); + case FieldType.Checkbox: + return CheckboxFilterPB.fromObject({ + condition: (data as CheckboxFilterData).condition, + }); + case FieldType.Checklist: + return ChecklistFilterPB.fromObject({ + condition: (data as ChecklistFilterData).condition, + }); + case FieldType.DateTime: + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return DateFilterPB.fromObject({ + condition: (data as DateFilterData).condition, + start: (data as DateFilterData).start, + end: (data as DateFilterData).end, + timestamp: (data as DateFilterData).timestamp, + }); } } @@ -66,13 +143,52 @@ export function pbToSelectFilterData(pb: SelectOptionFilterPB): SelectFilterData }; } +export function pbToNumberFilterData(pb: NumberFilterPB): NumberFilterData { + return { + condition: pb.condition, + content: pb.content, + }; +} + +export function pbToCheckboxFilterData(pb: CheckboxFilterPB): CheckboxFilterData { + return { + condition: pb.condition, + }; +} + +export function pbToChecklistFilterData(pb: ChecklistFilterPB): ChecklistFilterData { + return { + condition: pb.condition, + }; +} + +export function pbToDateFilterData(pb: DateFilterPB): DateFilterData { + return { + condition: pb.condition, + start: pb.start, + end: pb.end, + timestamp: pb.timestamp, + }; +} + export function bytesToFilterData(bytes: Uint8Array, fieldType: FieldType) { switch (fieldType) { case FieldType.RichText: + case FieldType.URL: return pbToTextFilterData(TextFilterPB.deserialize(bytes)); case FieldType.SingleSelect: case FieldType.MultiSelect: return pbToSelectFilterData(SelectOptionFilterPB.deserialize(bytes)); + case FieldType.Number: + return pbToNumberFilterData(NumberFilterPB.deserialize(bytes)); + case FieldType.Checkbox: + return pbToCheckboxFilterData(CheckboxFilterPB.deserialize(bytes)); + case FieldType.Checklist: + return pbToChecklistFilterData(ChecklistFilterPB.deserialize(bytes)); + case FieldType.DateTime: + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return pbToDateFilterData(DateFilterPB.deserialize(bytes)); } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/index.ts index 55e195606d..ac10d27d0a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/index.ts @@ -1,2 +1,3 @@ export * from './filter_types'; export * as filterService from './filter_service'; +export * as filterListeners from './filter_listeners'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/row/row_listeners.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/row/row_listeners.ts index 9405c43028..a1f975409e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/row/row_listeners.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/row/row_listeners.ts @@ -1,6 +1,7 @@ import { ReorderAllRowsPB, ReorderSingleRowPB, RowsChangePB, RowsVisibilityChangePB } from '@/services/backend'; import { Database } from '../database'; import { pbToRowMeta, RowMeta } from './row_types'; +import { didDeleteCells } from '$app/components/database/application/cell/cell_listeners'; const deleteRowsFromChangeset = (database: Database, changeset: RowsChangePB) => { changeset.deleted_rows.forEach((rowId) => { @@ -8,6 +9,8 @@ const deleteRowsFromChangeset = (database: Database, changeset: RowsChangePB) => if (index !== -1) { database.rowMetas.splice(index, 1); + // delete cells + didDeleteCells({ database, rowId }); } }); }; @@ -23,7 +26,7 @@ const updateRowsFromChangeset = (database: Database, changeset: RowsChangePB) => const found = database.rowMetas.find((rowMeta) => rowMeta.id === rowId); if (found) { - Object.assign(found, pbToRowMeta(rowMetaPB)); + Object.assign(found, rowMetaPB ? pbToRowMeta(rowMetaPB) : {}); } }); }; 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 143f6936f1..e234bf00ca 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 @@ -1,23 +1,63 @@ import { useCallback, useEffect, useState } from 'react'; -import { DatabaseNotification, FieldType } from '@/services/backend'; +import { DatabaseNotification } from '@/services/backend'; import { useNotification, useViewId } from '$app/hooks'; -import { cellService, Cell } from '../../application'; +import { cellService, Cell, Field } from '../../application'; +import { useDispatchCell, useSelectorCell } from '$app/components/database'; -export const useCell = (rowId: string, fieldId: string, fieldType: FieldType) => { +export const useCell = (rowId: string, field: Field) => { const viewId = useViewId(); - const [cell, setCell] = useState(undefined); + const { setCell } = useDispatchCell(); + const [loading, setLoading] = useState(false); + const cell = useSelectorCell(rowId, field.id); const fetchCell = useCallback(() => { - void cellService.getCell(viewId, rowId, fieldId, fieldType).then((data) => { + setLoading(true); + void cellService.getCell(viewId, rowId, field.id, field.type).then((data) => { + // cache cell setCell(data); + setLoading(false); }); - }, [viewId, rowId, fieldId, fieldType]); + }, [viewId, rowId, field.id, field.type, setCell]); useEffect(() => { - fetchCell(); - }, [fetchCell]); + // fetch cell if not cached + if (!cell && !loading) { + // fetch cell in next tick to avoid blocking + const timeout = setTimeout(fetchCell, 0); - useNotification(DatabaseNotification.DidUpdateCell, fetchCell, { id: `${rowId}:${fieldId}` }); + return () => { + clearTimeout(timeout); + }; + } + }, [fetchCell, cell, loading]); + + useNotification(DatabaseNotification.DidUpdateCell, fetchCell, { id: `${rowId}:${field.id}` }); return cell; }; + +export const useInputCell = (cell?: Cell) => { + const [editing, setEditing] = useState(false); + const [value, setValue] = useState(''); + const viewId = useViewId(); + const updateCell = useCallback(() => { + if (!cell) return; + const { rowId, fieldId } = cell; + + if (editing) { + if (value !== cell.data) { + void cellService.updateCell(viewId, rowId, fieldId, value); + } + + setEditing(false); + } + }, [cell, editing, value, viewId]); + + return { + updateCell, + editing, + setEditing, + value, + setValue, + }; +}; 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 941f3ad0da..c76e1e80cb 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 @@ -6,6 +6,11 @@ import { useCell } from './Cell.hooks'; import { TextCell } from './TextCell'; import { SelectCell } from './SelectCell'; import { CheckboxCell } from './CheckboxCell'; +import NumberCell from '$app/components/database/components/cell/NumberCell'; +import URLCell from '$app/components/database/components/cell/URLCell'; +import ChecklistCell from '$app/components/database/components/cell/ChecklistCell'; +import DateTimeCell from '$app/components/database/components/cell/DateTimeCell'; +import TimestampCell from '$app/components/database/components/cell/TimestampCell'; export interface CellProps { rowId: string; @@ -15,25 +20,44 @@ export interface CellProps { placeholder?: string; } +interface CellComponentProps { + field: Field; + cell: CellType; +} const getCellComponent = (fieldType: FieldType) => { switch (fieldType) { case FieldType.RichText: - return TextCell as FC<{ field: Field; cell?: CellType }>; + return TextCell as FC; case FieldType.SingleSelect: case FieldType.MultiSelect: - return SelectCell as FC<{ field: Field; cell?: CellType }>; + return SelectCell as FC; case FieldType.Checkbox: - return CheckboxCell as FC<{ field: Field; cell?: CellType }>; + return CheckboxCell as FC; + case FieldType.Checklist: + return ChecklistCell as FC; + case FieldType.Number: + return NumberCell as FC; + case FieldType.URL: + return URLCell as FC; + case FieldType.DateTime: + return DateTimeCell as FC; + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return TimestampCell as FC; default: return null; } }; export const Cell: FC = ({ rowId, field, ...props }) => { - const cell = useCell(rowId, field.id, field.type); + const cell = useCell(rowId, field); const Component = getCellComponent(field.type); + if (!cell) { + return
; + } + if (!Component) { return null; } 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 6aaee306f0..d795d94669 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 @@ -1,5 +1,4 @@ -import { Checkbox } from '@mui/material'; -import { FC, useCallback } from 'react'; +import React, { FC, useCallback } from 'react'; import { ReactComponent as CheckboxCheckSvg } from '$app/assets/database/checkbox-check.svg'; import { ReactComponent as CheckboxUncheckSvg } from '$app/assets/database/checkbox-uncheck.svg'; import { useViewId } from '$app/hooks'; @@ -7,25 +6,18 @@ import { cellService, CheckboxCell as CheckboxCellType, Field } from '../../appl export const CheckboxCell: FC<{ field: Field; - cell?: CheckboxCellType; + cell: CheckboxCellType; }> = ({ field, cell }) => { const viewId = useViewId(); - const checked = cell?.data === 'Yes'; + const checked = cell.data === 'Yes'; const handleClick = useCallback(() => { - if (!cell) return; void cellService.updateCell(viewId, cell.rowId, field.id, !checked ? 'Yes' : 'No'); }, [viewId, cell, field.id, checked]); return ( -
- } - checkedIcon={} - /> +
+ {checked ? : }
); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/ChecklistCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/ChecklistCell.tsx new file mode 100644 index 0000000000..a72b16b376 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/ChecklistCell.tsx @@ -0,0 +1,55 @@ +import React, { useState, Suspense, useMemo } from 'react'; +import { ChecklistCell as ChecklistCellType, ChecklistField } from '$app/components/database/application'; +import Typography from '@mui/material/Typography'; +import ChecklistCellActions from '$app/components/database/components/field_types/checklist/ChecklistCellActions'; + +interface Props { + field: ChecklistField; + cell: ChecklistCellType; +} + +function ChecklistCell({ cell }: Props) { + const value = cell?.data.percentage ?? 0; + + const [anchorEl, setAnchorEl] = useState(undefined); + const open = Boolean(anchorEl); + const handleClick = (e: React.MouseEvent) => { + setAnchorEl(e.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(undefined); + }; + + const result = useMemo(() => `${Math.round(value * 100)}%`, [value]); + + return ( + <> +
+ + {result} + +
+ + {open && ( + + )} + + + ); +} + +export default ChecklistCell; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/DateTimeCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/DateTimeCell.tsx new file mode 100644 index 0000000000..31f35afd5b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/DateTimeCell.tsx @@ -0,0 +1,55 @@ +import React, { Suspense, useRef, useState, useMemo } from 'react'; +import { DateTimeCell as DateTimeCellType, DateTimeField } from '$app/components/database/application'; +import DateTimeCellActions from '$app/components/database/components/field_types/date/DateTimeCellActions'; + +interface Props { + field: DateTimeField; + cell: DateTimeCellType; + placeholder?: string; +} +function DateTimeCell({ field, cell, placeholder }: Props) { + const isRange = cell.data.isRange; + const includeTime = cell.data.includeTime; + const [open, setOpen] = useState(false); + const ref = useRef(null); + + const handleClose = () => { + setOpen(false); + }; + + const handleClick = () => { + setOpen(true); + }; + + const content = useMemo(() => { + const { date, time, endDate, endTime } = cell.data; + + if (date) { + return ( + <> + {date} + {includeTime && time ? ' ' + time : ''} + {isRange && endDate ? ' - ' + endDate : ''} + {isRange && includeTime && endTime ? ' ' + endTime : ''} + + ); + } + + return
{placeholder}
; + }, [cell, includeTime, isRange, placeholder]); + + return ( + <> +
+ {content} +
+ + {open && ( + + )} + + + ); +} + +export default DateTimeCell; 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 index 73b88992f3..efb3b8c131 100644 --- 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 @@ -21,7 +21,12 @@ function ExpandButton({ cell, documentId, icon, visible }: Props) { return ( <> {visible && ( -
+
setOpen(true)} className={'h-6 w-6 text-sm'}> 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 new file mode 100644 index 0000000000..859537481e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/NumberCell.tsx @@ -0,0 +1,50 @@ +import React, { Suspense, useCallback, useMemo, useRef } from 'react'; +import { Field, NumberCell as NumberCellType } from '$app/components/database/application'; +import { CellText } from '$app/components/database/_shared'; +import EditNumberCellInput from '$app/components/database/components/field_types/number/EditNumberCellInput'; +import { useInputCell } from '$app/components/database/components/cell/Cell.hooks'; + +interface Props { + field: Field; + cell: NumberCellType; + placeholder?: string; +} + +function NumberCell({ field, cell, placeholder }: Props) { + const cellRef = useRef(null); + const { value, editing, updateCell, setEditing, setValue } = useInputCell(cell); + const content = useMemo(() => { + if (typeof cell.data === 'string' && cell.data) { + return cell.data; + } + + return
{placeholder}
; + }, [cell, placeholder]); + + const handleClick = useCallback(() => { + setValue(cell.data); + setEditing(true); + }, [cell, setEditing, setValue]); + + return ( + <> + +
{content}
+
+ + {editing && ( + + )} + + + ); +} + +export default NumberCell; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell.tsx new file mode 100644 index 0000000000..53a3171583 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell.tsx @@ -0,0 +1,76 @@ +import { FC, useCallback, useMemo, useState, Suspense, lazy } from 'react'; +import { MenuProps, Menu } from '@mui/material'; +import { SelectField, SelectCell as SelectCellType, SelectTypeOption } from '../../application'; +import { Tag } from '../field_types/select/Tag'; +import { useTypeOption } from '$app/components/database'; + +const SelectCellActions = lazy( + () => import('$app/components/database/components/field_types/select/select_cell_actions/SelectCellActions') +); +const menuProps: Partial = { + classes: { + list: 'py-5', + }, + anchorOrigin: { + vertical: 'bottom', + horizontal: 'left', + }, + transformOrigin: { + vertical: 'top', + horizontal: 'left', + }, +}; + +export const SelectCell: FC<{ + field: SelectField; + cell: SelectCellType; + placeholder?: string; +}> = ({ field, cell, placeholder }) => { + const [anchorEl, setAnchorEl] = useState(null); + const selectedIds = useMemo(() => cell.data?.selectedOptionIds ?? [], [cell]); + const open = Boolean(anchorEl); + const handleClose = useCallback(() => { + setAnchorEl(null); + }, []); + + const typeOption = useTypeOption(field.id); + + const renderSelectedOptions = useCallback( + (selected: string[]) => + selected + .map((id) => typeOption.options?.find((option) => option.id === id)) + .map((option) => option && ), + [typeOption] + ); + + return ( +
+
{ + setAnchorEl(e.currentTarget); + }} + className={'flex h-full w-full cursor-pointer items-center gap-2 overflow-x-hidden px-2 py-1'} + > + {selectedIds.length === 0 ? ( +
{placeholder}
+ ) : ( + renderSelectedOptions(selectedIds) + )} +
+ + {open ? ( + + + + ) : null} + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/CreateOption.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/CreateOption.tsx deleted file mode 100644 index 1d7a5249b4..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/CreateOption.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { MenuItem, MenuItemProps } from '@mui/material'; -import { FC } from 'react'; -import { Tag } from './Tag'; - -export interface CreateOptionProps { - label: React.ReactNode; - onClick?: MenuItemProps['onClick']; -} - -export const CreateOption: FC = ({ - label, - onClick, -}) => { - return ( - - - - ); -}; \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectCell.tsx deleted file mode 100644 index 5488b44810..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectCell.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { FC, FormEvent, useCallback, useMemo, useState } from 'react'; -import { t } from 'i18next'; -import { ListSubheader, Select, OutlinedInput, SelectChangeEvent, InputBase, MenuProps, MenuItem } from '@mui/material'; -import { FieldType } from '@/services/backend'; -import { useViewId } from '$app/hooks'; -import { cellService, SelectField, SelectCell as SelectCellType } from '../../../application'; -import { Tag } from './Tag'; -import { CreateOption } from './CreateOption'; -import { SelectOptionItem } from './SelectOptionItem'; - -const menuProps: Partial = { - classes: { - list: 'py-5', - }, - anchorOrigin: { - vertical: 'bottom', - horizontal: 'left', - }, - transformOrigin: { - vertical: 'top', - horizontal: 'left', - }, -}; - -export const SelectCell: FC<{ - field: SelectField; - cell?: SelectCellType; -}> = ({ field, cell }) => { - const [open, setOpen] = useState(false); - const rowId = cell?.rowId; - const viewId = useViewId(); - const options = useMemo(() => field.typeOption.options ?? [], [field.typeOption.options]); - const selectedIds = useMemo(() => cell?.data.selectedOptionIds ?? [], [cell?.data.selectedOptionIds]); - const [newOptionName, setNewOptionName] = useState(''); - const filteredOptions = useMemo( - () => - options.filter((option) => { - return option.name.toLowerCase().includes(newOptionName.toLowerCase()); - }), - [options, newOptionName] - ); - - const shouldCreateOption = !!newOptionName && filteredOptions.length === 0; - - const handleInput = useCallback((event: FormEvent) => { - const value = (event.target as HTMLInputElement).value; - - setNewOptionName(value); - }, []); - - const handleClose = useCallback(() => { - setNewOptionName(''); - setOpen(false); - }, []); - - const handleChange = (event: SelectChangeEvent) => { - if (!cell || !rowId) return; - const { - target: { value }, - } = event; - - const current = Array.isArray(value) ? value : [value]; - const prev = cell.data.selectedOptionIds; - const deleteOptionIds = prev?.filter((id) => current.find((cur) => cur === id) === undefined); - - void cellService.updateSelectCell(viewId, rowId, field.id, { - insertOptionIds: current, - deleteOptionIds, - }); - }; - - const handleNewTagClick = async () => { - if (!cell || !rowId) return; - const exist = options.find((option) => option.name.toLowerCase() === newOptionName.toLowerCase()); - - if (exist) { - return cellService.updateSelectCell(viewId, rowId, field.id, { - insertOptionIds: [exist.id], - }); - } - - // const option = await cellService.createSelectOption(viewId, field.id, newOptionName); - - // await cellService.insertOrUpdateSelectOption(viewId, field.id, [option], rowId); - }; - - const searchInput = ( - - - - ); - - const renderSelectedOptions = useCallback( - (selected: string[]) => - selected - .map((id) => options.find((option) => option.id === id)) - .map((option) => option && ), - [options] - ); - - return ( -
-
{ - setOpen(true); - }} - className={'absolute left-0 top-0 flex h-full w-full items-center gap-2 px-4 py-1'} - > - {renderSelectedOptions(selectedIds)} -
- {open ? ( - - ) : null} -
- ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectOptionItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectOptionItem.tsx deleted file mode 100644 index f320731af0..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectOptionItem.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FC, MouseEventHandler, useCallback, useRef, useState } from 'react'; -import { IconButton } from '@mui/material'; -import { ReactComponent as DetailsSvg } from '$app/assets/details.svg'; -import { SelectOption } from '../../../application'; -import { SelectOptionMenu } from './SelectOptionMenu'; -import { Tag } from './Tag'; - -export interface SelectOptionItemProps { - option: SelectOption; -} - -export const SelectOptionItem: FC = ({ - option, -}) => { - const [open, setOpen] = useState(false); - const anchorEl = useRef(null); - - const handleClick = useCallback>((event) => { - event.stopPropagation(); - anchorEl.current = event.target as HTMLButtonElement; - setOpen(true); - }, []); - - return ( - <> -
- -
- - - - {open && ( - setOpen(false), - }} - /> - )} - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectOptionMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectOptionMenu.tsx deleted file mode 100644 index 310881017a..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectOptionMenu.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { FC } from 'react'; -import { t } from 'i18next'; -import { - Divider, - ListSubheader, - Menu, - MenuItem, - MenuProps, - OutlinedInput, -} from '@mui/material'; -import { SelectOptionColorPB } from '@/services/backend'; -import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg'; -import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg'; -import { SelectOption } from '../../../application'; -import { SelectOptionColorMap, SelectOptionColorTextMap } from './constants'; - -interface SelectOptionMenuProps { - option: SelectOption; - open: boolean; - MenuProps?: Partial; -} - -const Colors = [ - SelectOptionColorPB.Purple, - SelectOptionColorPB.Pink, - SelectOptionColorPB.LightPink, - SelectOptionColorPB.Orange, - SelectOptionColorPB.Yellow, - SelectOptionColorPB.Lime, - SelectOptionColorPB.Green, - SelectOptionColorPB.Aqua, - SelectOptionColorPB.Blue, -]; - -export const SelectOptionMenu: FC = ({ - open, - option, - MenuProps: menuProps, -}) => { - return ( - - - - - - - {t('grid.selectOption.deleteTag')} - - - {t('grid.selectOption.colorPanelTitle')} - {Colors.map(color => ( - - - - {t(`grid.selectOption.${SelectOptionColorTextMap[color]}`)} - - {option.color === color && ( - - )} - - ))} - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/index.ts deleted file mode 100644 index 745d3fb1a2..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SelectCell'; 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 c1b870de84..ccf3bfc430 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,57 +1,45 @@ -import { FC, FormEventHandler, Suspense, lazy, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { useViewId } from '$app/hooks'; -import { cellService, Field, TextCell as TextCellType } from '../../application'; +import { FC, FormEventHandler, Suspense, lazy, useCallback, useEffect, useRef, useMemo } from 'react'; +import { Field, 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/cell/EditTextCellInput')); +const EditTextCellInput = lazy(() => import('$app/components/database/components/field_types/text/EditTextCellInput')); export const TextCell: FC<{ field: Field; - cell?: TextCellType; + cell: TextCellType; documentId?: string; icon?: string; placeholder?: string; -}> = ({ field, cell, documentId, icon, placeholder }) => { +}> = ({ field, documentId, icon, placeholder, cell }) => { const isPrimary = field.isPrimary; - const viewId = useViewId(); const cellRef = useRef(null); - const [editing, setEditing] = useState(false); - const [text, setText] = useState(''); - const [width, setWidth] = useState(undefined); + const { value, editing, updateCell, setEditing, setValue } = useInputCell(cell); + const { hoverRowId } = useGridUIStateSelector(); const isHover = hoverRowId === cell?.rowId; const { setRowHover } = useGridUIStateDispatcher(); - const textareaRef = useRef(null); const showExpandIcon = cell && !editing && isHover && isPrimary; const handleClose = () => { if (!cell) return; - if (editing) { - if (text !== cell.data) { - void cellService.updateCell(viewId, cell.rowId, field.id, text); - } - - setEditing(false); - } + updateCell(); }; const handleClick = useCallback(() => { if (!cell) return; - setText(cell.data); + setValue(cell.data); setEditing(true); - }, [cell]); + }, [cell, setEditing, setValue]); - const handleInput = useCallback>((event) => { - setText((event.target as HTMLTextAreaElement).value); - }, []); - - useLayoutEffect(() => { - if (cellRef.current) { - setWidth(cellRef.current.clientWidth); - } - }, [editing]); + const handleInput = useCallback>( + (event) => { + setValue((event.target as HTMLTextAreaElement).value); + }, + [setValue] + ); useEffect(() => { if (editing) { @@ -59,20 +47,27 @@ export const TextCell: FC<{ } }, [editing, setRowHover]); - useEffect(() => { - if (textareaRef.current) { - textareaRef.current.focus(); - // set the cursor to the end of the text - textareaRef.current.setSelectionRange(textareaRef.current.value.length, textareaRef.current.value.length); + const content = useMemo(() => { + if (cell && typeof cell.data === 'string' && cell.data) { + return cell.data; } - }, []); + + return
{placeholder}
; + }, [cell, placeholder]); return ( - <> - -
+
+ +
{icon &&
{icon}
} - {cell?.data ||
{placeholder}
} + {content}
@@ -81,13 +76,13 @@ export const TextCell: FC<{ )} - +
); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TimestampCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TimestampCell.tsx new file mode 100644 index 0000000000..f7aeec9f2c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TimestampCell.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { CreatedTimeField, LastEditedTimeField, TimeStampCell } from '$app/components/database/application'; + +interface Props { + field: LastEditedTimeField | CreatedTimeField; + cell: TimeStampCell; +} + +function TimestampCell({ cell }: Props) { + return
{cell.data.dataTime}
; +} + +export default TimestampCell; 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 new file mode 100644 index 0000000000..5ace914589 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/URLCell.tsx @@ -0,0 +1,92 @@ +import React, { FormEventHandler, lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useInputCell } from '$app/components/database/components/cell/Cell.hooks'; +import { Field, UrlCell as URLCellType } from '$app/components/database/application'; +import { CellText } from '$app/components/database/_shared'; + +const EditTextCellInput = lazy(() => import('$app/components/database/components/field_types/text/EditTextCellInput')); + +const pattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w.-]*)*\/?$/; + +interface Props { + field: Field; + cell: URLCellType; + placeholder?: string; +} + +function UrlCell({ field, cell, placeholder }: Props) { + const [isUrl, setIsUrl] = useState(false); + const cellRef = useRef(null); + const { value, editing, updateCell, setEditing, setValue } = useInputCell(cell); + const handleClick = useCallback(() => { + setValue(cell.data.content || ''); + setEditing(true); + }, [cell, setEditing, setValue]); + + const handleClose = () => { + updateCell(); + }; + + const handleInput = useCallback>( + (event) => { + setValue((event.target as HTMLTextAreaElement).value); + }, + [setValue] + ); + + useEffect(() => { + if (editing) return; + const str = cell.data.content; + + if (!str) return; + const isUrl = pattern.test(str); + + setIsUrl(isUrl); + }, [cell, editing]); + + const content = useMemo(() => { + const str = cell.data.content; + + if (str) { + if (isUrl) { + return ( + + {str} + + ); + } + + return str; + } + + return
{placeholder}
; + }, [isUrl, cell, placeholder]); + + return ( + <> + +
{content}
+
+ + {editing && ( + + )} + + + ); +} + +export default UrlCell; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseSettings.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseSettings.tsx index 969daed1b9..b6672ec007 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseSettings.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseSettings.tsx @@ -31,4 +31,4 @@ function DatabaseSettings(props: Props) { ); } -export default React.memo(DatabaseSettings); +export default DatabaseSettings; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/FilterSettings.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/FilterSettings.tsx index d8dea4f922..61d438c9eb 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/FilterSettings.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/FilterSettings.tsx @@ -1,13 +1,13 @@ import React, { useState } from 'react'; import { TextButton } from '$app/components/database/components/tab_bar/TextButton'; import { useTranslation } from 'react-i18next'; -import { useDatabase } from '$app/components/database'; +import { useFiltersCount } from '$app/components/database'; import FilterFieldsMenu from '$app/components/database/components/filter/FilterFieldsMenu'; function FilterSettings({ onToggleCollection }: { onToggleCollection: (forceOpen?: boolean) => void }) { const { t } = useTranslation(); - const { filters } = useDatabase(); - const highlight = filters && filters.length > 0; + const filtersCount = useFiltersCount(); + const highlight = filtersCount > 0; const [filterAnchorEl, setFilterAnchorEl] = useState(null); const open = Boolean(filterAnchorEl); @@ -31,6 +31,10 @@ function FilterSettings({ onToggleCollection }: { onToggleCollection: (forceOpen open={open} anchorEl={filterAnchorEl} onClose={() => setFilterAnchorEl(null)} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right', + }} /> ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/Properties.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/Properties.tsx index f62294a215..48cd138381 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/Properties.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/Properties.tsx @@ -19,10 +19,13 @@ function Properties({ onItemClick }: PropertiesProps) { onItemClick(field)} - className={'flex w-full items-center justify-between'} + className={'flex w-full items-center justify-between overflow-hidden px-1.5'} key={field.id} > - +
+ +
+
{field.visibility !== FieldVisibility.AlwaysHidden ? : }
))} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SortSettings.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SortSettings.tsx index 099f6c7cab..f256f4bd2e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SortSettings.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SortSettings.tsx @@ -39,6 +39,10 @@ function SortSettings({ onToggleCollection }: Props) { open={open} anchorEl={sortAnchorEl} onClose={handleClose} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right', + }} /> ); 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 292689ba14..fcdcf5c915 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 @@ -4,7 +4,8 @@ import RecordDocument from '$app/components/database/components/edit_record/Reco 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 { ViewLayoutPB } from '@/services/backend'; +import { ErrorCode, ViewLayoutPB } from '@/services/backend'; +import { Log } from '$app/utils/log'; interface Props { cell: TextCell; @@ -26,7 +27,7 @@ function EditRecord({ documentId: id, cell, icon }: Props) { // Record not found // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - if (e.code === 3) { + if (e.code === ErrorCode.RecordNotFound) { try { const page = await controller.createOrphanPage({ name: '', @@ -35,7 +36,7 @@ function EditRecord({ documentId: id, cell, icon }: Props) { setPage(page); } catch (e) { - console.error(e); + Log.error(e); } } } 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 index 1fa8ae2dd2..ece3405104 100644 --- 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 @@ -1,25 +1,28 @@ -import React, { MouseEvent, useCallback, useState } from 'react'; -import { Field, fieldService } from '$app/components/database/application'; +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 [updateField, setUpdateField] = useState(null); + 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); - setUpdateField(field); + setUpdateFieldId(field.id); setAnchorEl(e.target as HTMLButtonElement); } catch (e) { // toast.error(t('grid.field.newPropertyFail')); @@ -39,7 +42,7 @@ function NewProperty() { anchorEl={anchorEl} open={open} onClose={() => { - setUpdateField(null); + setUpdateFieldId(''); setAnchorEl(null); }} /> 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 e5fdee7b26..c267f5f84d 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 @@ -32,7 +32,7 @@ function Property({ field, rowId, ishovered, onHover, ...props }: Props, ref: Re onMouseLeave={() => { onHover(null); }} - className={'relative flex gap-6 rounded hover:bg-content-blue-50'} + className={'relative flex items-start gap-6 rounded hover:bg-content-blue-50'} key={field.id} {...props} > 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 193fca0303..13067279a3 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 @@ -21,7 +21,7 @@ function PropertyName({ field, openMenu, onOpenMenu, onCloseMenu }: Props) { e.preventDefault(); onOpenMenu(); }} - className={'flex w-[200px] cursor-pointer items-center'} + className={'flex min-h-[36px] w-[200px] cursor-pointer items-center'} onClick={onOpenMenu} > @@ -31,4 +31,4 @@ function PropertyName({ field, openMenu, onOpenMenu, onCloseMenu }: Props) { ); } -export default React.memo(PropertyName); +export default PropertyName; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyValue.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyValue.tsx index 8455d47e46..90f8147f9f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyValue.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyValue.tsx @@ -1,14 +1,31 @@ -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Cell } from '$app/components/database/components'; import { Field } from '$app/components/database/application'; import { useTranslation } from 'react-i18next'; function PropertyValue(props: { rowId: string; field: Field }) { const { t } = useTranslation(); + const ref = useRef(null); + const [width, setWidth] = useState(props.field.width); + useEffect(() => { + const el = ref.current; + + if (!el) return; + const width = el.getBoundingClientRect().width; + + setWidth(width); + }, []); return ( -
- +
+
); } 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 97f9bb593b..00b990de6b 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,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Field, fieldService, TextCell } from '$app/components/database/application'; import { useDatabase } from '$app/components/database'; import { FieldVisibility } from '@/services/backend'; @@ -11,6 +11,7 @@ import PropertyList from '$app/components/database/components/edit_record/record import NewProperty from '$app/components/database/components/edit_record/record_properties/NewProperty'; import { useViewId } from '$app/hooks'; import { DragDropContext, Droppable, DropResult, OnDragEndResponder } from 'react-beautiful-dnd'; +import { useTranslation } from 'react-i18next'; interface Props { documentId?: string; @@ -28,6 +29,7 @@ const reorder = (list: Field[], startIndex: number, endIndex: number) => { }; function RecordProperties({ documentId, cell }: Props) { + const { t } = useTranslation(); const viewId = useViewId(); const { fieldId, rowId } = cell; const { fields } = useDatabase(); @@ -41,8 +43,18 @@ function RecordProperties({ documentId, cell }: Props) { }); }, [fieldId, fields, showHiddenFields]); + const hiddenFieldsCount = useMemo(() => { + return fields.filter((field) => { + return field.visibility === FieldVisibility.AlwaysHidden; + }).length; + }, [fields]); + const [state, setState] = useState(properties); + useEffect(() => { + setState(properties); + }, [properties]); + // move the field in the database const onMoveProperty = useCallback( async (fieldId: string, prevId?: string) => { @@ -103,19 +115,31 @@ function RecordProperties({ documentId, cell }: Props) { )} - + { + // show the button only if there are hidden fields + hiddenFieldsCount > 0 && ( + + ) + } +
); } -export default React.memo(RecordProperties); +export default RecordProperties; 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 index f96d07e3f9..2d77408cb9 100644 --- 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 @@ -6,15 +6,11 @@ export interface FieldProps { field: FieldType; } -export const Field: FC = ({ - field, -}) => { +export const Field: FC = ({ field }) => { return ( -
- - - {field.name} - +
+ + {field.name}
); }; 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/field/FieldList.tsx index 0cd248fd46..03e197c601 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldList.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldList.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useMemo, useState } from 'react'; -import { Input, MenuItem } from '@mui/material'; +import { OutlinedInput, MenuItem, MenuList } from '@mui/material'; import { Field } from '$app/components/database/components/field/Field'; import { Field as FieldType } from '../../application'; import { useDatabase } from '$app/components/database'; @@ -26,34 +26,37 @@ function FieldList({ showSearch, onItemClick, searchPlaceholder }: FieldListProp const searchInput = useMemo(() => { return showSearch ? ( -
- +
+
) : null; }, [onInputChange, searchPlaceholder, showSearch]); const emptyList = useMemo(() => { return fieldsResult.length === 0 ? ( -
No fields found
+
No fields found
) : null; }, [fieldsResult]); return ( - <> +
{searchInput} {emptyList} - {fieldsResult.map((field) => ( - { - onItemClick?.(event, field); - }} - > - - - ))} - + + {fieldsResult.map((field) => ( + { + onItemClick?.(event, field); + }} + > + + + ))} + +
); } 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 index 48c25cb59d..c35b0908fc 100644 --- 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 @@ -1,10 +1,14 @@ -import { Divider, Menu, MenuItem, MenuProps, OutlinedInput } from '@mui/material'; +import { Divider, MenuList, MenuProps } from '@mui/material'; import { ChangeEventHandler, FC, useCallback, useState } from 'react'; -import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; import { useViewId } from '$app/hooks'; import { Field, fieldService } from '../../application'; import { FieldMenuActions } from './FieldMenuActions'; -import { FieldTypeText, FieldTypeSvg } from '$app/components/database/components/field/index'; +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; @@ -29,44 +33,60 @@ export const FieldMenu: FC = ({ field, anchorEl, open, onClo }); } catch (e) { // TODO - console.error(`change field ${field.id} name from '${field.name}' to ${inputtingName} fail`, e); + Log.error(`change field ${field.id} name from '${field.name}' to ${inputtingName} fail`, e); } } }, [viewId, field, inputtingName]); - const fieldNameInput = ( - - ); - - const fieldTypeSelect = ( - - - - - - - - ); - const isPrimary = field.isPrimary; - return ( - - {fieldNameInput} - {!isPrimary && ( -
- {fieldTypeSelect} - -
- )} + 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] + ); - onClose()} fieldId={field.id} /> -
+ return ( + + + +
+ {!isPrimary && ( + <> + + + + )} + + onClose()} fieldId={field.id} /> +
+
+
); }; 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/field/FieldSelect.tsx index 24eb42a78b..1aa3022845 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldSelect.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldSelect.tsx @@ -30,9 +30,12 @@ export const FieldSelect: FC = ({ onChange, ...props }) => { alignItems: 'center', }, }} + MenuProps={{ + className: 'max-w-[150px]', + }} > {fields.map((field) => ( - + ))} 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/field/FieldTypeMenu.tsx index dbe70c76b2..8d23196ebd 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeMenu.tsx @@ -2,6 +2,8 @@ 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 { Field } from '$app/components/database/application'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg'; const FieldTypeGroup = [ { @@ -14,15 +16,21 @@ const FieldTypeGroup = [ FieldType.DateTime, FieldType.Checkbox, FieldType.Checklist, + FieldType.URL, ], }, { name: 'Advanced', - types: [FieldType.LastEditedTime], + types: [FieldType.LastEditedTime, FieldType.CreatedTime], }, ]; -export const FieldTypeMenu: FC = (props) => { +export const FieldTypeMenu: FC< + MenuProps & { + field: Field; + onClickItem?: (type: FieldType) => void; + } +> = ({ field, onClickItem, ...props }) => { const PopoverClasses = useMemo( () => ({ ...props.PopoverClasses, @@ -38,11 +46,12 @@ export const FieldTypeMenu: FC = (props) => { {group.name} , group.types.map((type) => ( - + onClickItem?.(type)} key={type} dense className={'flex justify-between'}> - + + {type === field.type && } )), index < FieldTypeGroup.length - 1 && , 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/field/FieldTypeMenuExtension.tsx new file mode 100644 index 0000000000..84dac7724e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeMenuExtension.tsx @@ -0,0 +1,26 @@ +import React, { useMemo } from 'react'; +import { FieldType } from '@/services/backend'; +import { DateTimeField, Field, NumberField, SelectField } from '$app/components/database/application'; +import SelectFieldActions from '$app/components/database/components/field_types/select/select_field_actions/SelectFieldActions'; +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 }) { + return useMemo(() => { + switch (field.type) { + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return ; + case FieldType.Number: + return ; + case FieldType.DateTime: + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return ; + default: + return null; + } + }, [field]); +} + +export default FieldTypeMenuExtension; 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/field/FieldTypeSelect.tsx new file mode 100644 index 0000000000..aa87eee94b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeSelect.tsx @@ -0,0 +1,57 @@ +import React, { useRef, useState } from 'react'; +import { FieldTypeSvg } from '$app/components/database/components/field/FieldTypeSvg'; +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 { FieldType } from '@/services/backend'; +import { FieldTypeText } from '$app/components/database/components/field/FieldTypeText'; + +interface Props { + field: Field; + onUpdateFieldType: (type: FieldType) => void; +} +function FieldTypeSelect({ field, onUpdateFieldType }: Props) { + const [expanded, setExpanded] = useState(false); + const ref = useRef(null); + + return ( +
+ { + setExpanded(!expanded); + }} + className={'px-23 mx-0'} + > + + + + + + + {expanded && ( + { + setExpanded(false); + }} + /> + )} +
+ ); +} + +export default FieldTypeSelect; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeText.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeText.tsx index c9904af711..6428cfcca6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeText.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeText.tsx @@ -1,39 +1,26 @@ import { FieldType } from '@/services/backend'; import { useTranslation } from 'react-i18next'; -import { useCallback } from 'react'; +import { useMemo } from 'react'; export const FieldTypeText = ({ type }: { type: FieldType }) => { const { t } = useTranslation(); - const getText = useCallback( - (type: FieldType) => { - switch (type) { - case FieldType.RichText: - return t('grid.field.textFieldName'); - case FieldType.Number: - return t('grid.field.numberFieldName'); - case FieldType.DateTime: - return t('grid.field.dateFieldName'); - case FieldType.SingleSelect: - return t('grid.field.singleSelectFieldName'); - case FieldType.MultiSelect: - return t('grid.field.multiSelectFieldName'); - case FieldType.Checkbox: - return t('grid.field.checkboxFieldName'); - case FieldType.URL: - return t('grid.field.urlFieldName'); - case FieldType.Checklist: - return t('grid.field.checklistFieldName'); - case FieldType.LastEditedTime: - return t('grid.field.updatedAtFieldName'); - case FieldType.CreatedTime: - return t('grid.field.createdAtFieldName'); - default: - return ''; - } - }, - [t] - ); + const text = useMemo(() => { + const map = { + [FieldType.RichText]: t('grid.field.textFieldName'), + [FieldType.Number]: t('grid.field.numberFieldName'), + [FieldType.DateTime]: t('grid.field.dateFieldName'), + [FieldType.SingleSelect]: t('grid.field.singleSelectFieldName'), + [FieldType.MultiSelect]: t('grid.field.multiSelectFieldName'), + [FieldType.Checkbox]: t('grid.field.checkboxFieldName'), + [FieldType.URL]: t('grid.field.urlFieldName'), + [FieldType.Checklist]: t('grid.field.checklistFieldName'), + [FieldType.LastEditedTime]: t('grid.field.updatedAtFieldName'), + [FieldType.CreatedTime]: t('grid.field.createdAtFieldName'), + }; - return <>{getText(type)}; + return map[type] || 'unknown'; + }, [t, type]); + + return
{text}
; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/AddNewOption.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/AddNewOption.tsx new file mode 100644 index 0000000000..ff3c5db172 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/AddNewOption.tsx @@ -0,0 +1,41 @@ +import React, { useState } from 'react'; +import { updateChecklistCell } from '$app/components/database/application/cell/cell_service'; +import { useViewId } from '$app/hooks'; +import { ReactComponent as AddIcon } from '$app/assets/add.svg'; +import { IconButton } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +function AddNewOption({ rowId, fieldId }: { rowId: string; fieldId: string }) { + const { t } = useTranslation(); + const [value, setValue] = useState(''); + const viewId = useViewId(); + const createOption = async () => { + await updateChecklistCell(viewId, rowId, fieldId, { + insertOptions: [value], + }); + setValue(''); + }; + + return ( +
+ { + if (e.key === 'Enter') { + void createOption(); + } + }} + value={value} + onChange={(e) => { + setValue(e.target.value); + }} + /> + + + +
+ ); +} + +export default AddNewOption; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistCellActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistCellActions.tsx new file mode 100644 index 0000000000..ae2046d76f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistCellActions.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import { LinearProgressWithLabel } from '$app/components/database/components/field_types/checklist/LinearProgressWithLabel'; +import { Divider } from '@mui/material'; +import { ChecklistCell as ChecklistCellType } from '$app/components/database/application'; +import ChecklistItem from '$app/components/database/components/field_types/checklist/ChecklistItem'; +import AddNewOption from '$app/components/database/components/field_types/checklist/AddNewOption'; + +function ChecklistCellActions({ + cell, + ...props +}: PopoverProps & { + cell: ChecklistCellType; +}) { + const { fieldId, rowId } = cell; + const { percentage, selectedOptions = [], options } = cell.data; + + return ( + + +
+ {options?.map((option) => { + return ( + + ); + })} +
+ + + +
+ ); +} + +export default ChecklistCellActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistItem.tsx new file mode 100644 index 0000000000..076dc854ea --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistItem.tsx @@ -0,0 +1,80 @@ +import React, { useState } from 'react'; +import { SelectOption } from '$app/components/database/application'; +import { Checkbox, IconButton } from '@mui/material'; +import { updateChecklistCell } from '$app/components/database/application/cell/cell_service'; +import { useViewId } from '$app/hooks'; +import { ReactComponent as DeleteIcon } from '$app/assets/delete.svg'; +import { ReactComponent as CheckboxCheckSvg } from '$app/assets/database/checkbox-check.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$app/assets/database/checkbox-uncheck.svg'; + +function ChecklistItem({ + checked, + option, + rowId, + fieldId, +}: { + checked: boolean; + option: SelectOption; + rowId: string; + fieldId: string; +}) { + const [hover, setHover] = useState(false); + const [value, setValue] = useState(option.name); + const viewId = useViewId(); + const updateText = async () => { + await updateChecklistCell(viewId, rowId, fieldId, { + updateOptions: [ + { + ...option, + name: value, + }, + ], + }); + }; + + const onCheckedChange = async () => { + void updateChecklistCell(viewId, rowId, fieldId, { + selectedOptionIds: [option.id], + }); + }; + + const deleteOption = async () => { + await updateChecklistCell(viewId, rowId, fieldId, { + deleteOptionIds: [option.id], + }); + }; + + return ( +
{ + setHover(true); + }} + onMouseLeave={() => { + setHover(false); + }} + className={`flex items-center justify-between gap-2 rounded p-1 text-sm ${hover ? 'bg-fill-list-hover' : ''}`} + > + } + checkedIcon={} + /> + { + setValue(e.target.value); + }} + /> + + + +
+ ); +} + +export default ChecklistItem; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/LinearProgressWithLabel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/LinearProgressWithLabel.tsx new file mode 100644 index 0000000000..e8b9c95a44 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/LinearProgressWithLabel.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +export function LinearProgressWithLabel(props: LinearProgressProps & { value: number }) { + return ( + + + + + + {`${Math.round(props.value * 100)}%`} + + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/CustomCalendar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/CustomCalendar.tsx new file mode 100644 index 0000000000..d2b904a4a6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/CustomCalendar.tsx @@ -0,0 +1,87 @@ +import React, { useEffect, useState } from 'react'; +import DatePicker, { ReactDatePickerCustomHeaderProps } from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { ReactComponent as LeftSvg } from '$app/assets/arrow-left.svg'; +import { ReactComponent as RightSvg } from '$app/assets/arrow-right.svg'; +import { IconButton } from '@mui/material'; + +function CustomCalendar({ + handleChange, + isRange, + timestamp, + endTimestamp, +}: { + handleChange: (params: { date?: number; endDate?: number }) => void; + isRange: boolean; + timestamp: number; + endTimestamp: number; +}) { + const [startDate, setStartDate] = useState(new Date(timestamp * 1000)); + const [endDate, setEndDate] = useState(new Date(endTimestamp * 1000)); + + useEffect(() => { + if (!isRange) return; + setEndDate(new Date(endTimestamp * 1000)); + }, [isRange, endTimestamp]); + + useEffect(() => { + setStartDate(new Date(timestamp * 1000)); + }, [timestamp]); + + return ( +
+ { + return ( +
+
+ {dayjs(props.date).format('MMMM YYYY')} +
+ +
+ + + + + + +
+
+ ); + }} + selected={startDate} + onChange={(dates) => { + if (!dates) return; + if (isRange) { + const [start, end] = dates as [Date | null, Date | null]; + + setStartDate(start); + setEndDate(end); + if (!start || !end) return; + handleChange({ + date: start.getTime() / 1000, + endDate: end.getTime() / 1000, + }); + } else { + const date = dates as Date; + + setStartDate(date); + handleChange({ + date: date.getTime() / 1000, + }); + } + }} + startDate={isRange ? startDate : null} + endDate={isRange ? endDate : null} + selectsRange={isRange} + inline + /> +
+ ); +} + +export default CustomCalendar; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateFormat.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateFormat.tsx new file mode 100644 index 0000000000..971c7d8bda --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateFormat.tsx @@ -0,0 +1,72 @@ +import React, { useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { MenuItem, Menu } from '@mui/material'; +import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg'; + +import { DateFormatPB } from '@/services/backend'; + +interface Props { + value: DateFormatPB; + onChange: (value: DateFormatPB) => void; +} + +function DateFormat({ value, onChange }: Props) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + const dateFormatMap = useMemo( + () => ({ + [DateFormatPB.Friendly]: t('grid.field.dateFormatFriendly'), + [DateFormatPB.ISO]: t('grid.field.dateFormatISO'), + [DateFormatPB.US]: t('grid.field.dateFormatUS'), + [DateFormatPB.Local]: t('grid.field.dateFormatLocal'), + [DateFormatPB.DayMonthYear]: t('grid.field.dateFormatDayMonthYear'), + }), + [t] + ); + + const handleClick = (option: DateFormatPB) => { + onChange(option); + setOpen(false); + }; + + return ( + <> + setOpen(true)} + > + {t('grid.field.dateFormat')} + + + setOpen(false)} + > + {Object.keys(dateFormatMap).map((option) => { + const optionValue = Number(option) as DateFormatPB; + + return ( + handleClick(optionValue)} + > + {dateFormatMap[optionValue]} + {value === optionValue && } + + ); + })} + + + ); +} + +export default DateFormat; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeCellActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeCellActions.tsx new file mode 100644 index 0000000000..d12b0b8cfd --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeCellActions.tsx @@ -0,0 +1,153 @@ +import React, { useCallback, useMemo } from 'react'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import { DateTimeCell, DateTimeField, DateTimeTypeOption } from '$app/components/database/application'; +import { useViewId } from '$app/hooks'; +import { useTranslation } from 'react-i18next'; +import { updateDateCell } from '$app/components/database/application/cell/cell_service'; +import { Divider, MenuItem, MenuList } from '@mui/material'; +import dayjs from 'dayjs'; +import RangeSwitch from '$app/components/database/components/field_types/date/RangeSwitch'; +import CustomCalendar from '$app/components/database/components/field_types/date/CustomCalendar'; +import IncludeTimeSwitch from '$app/components/database/components/field_types/date/IncludeTimeSwitch'; +import DateTimeFormatSelect from '$app/components/database/components/field_types/date/DateTimeFormatSelect'; +import DateTimeSet from '$app/components/database/components/field_types/date/DateTimeSet'; +import { useTypeOption } from '$app/components/database'; +import { getDateFormat, getTimeFormat } from '$app/components/database/components/field_types/date/utils'; + +function DateTimeCellActions({ + cell, + field, + ...props +}: PopoverProps & { + field: DateTimeField; + cell: DateTimeCell; +}) { + const typeOption = useTypeOption(field.id); + + const timeFormat = useMemo(() => { + return getTimeFormat(typeOption.timeFormat); + }, [typeOption.timeFormat]); + + const dateFormat = useMemo(() => { + return getDateFormat(typeOption.dateFormat); + }, [typeOption.dateFormat]); + + const { includeTime } = cell.data; + + const timestamp = useMemo(() => cell.data.timestamp || dayjs().unix(), [cell.data.timestamp]); + const endTimestamp = useMemo(() => cell.data.endTimestamp || dayjs().unix(), [cell.data.endTimestamp]); + const time = useMemo(() => cell.data.time || dayjs().format(timeFormat), [cell.data.time, timeFormat]); + const endTime = useMemo(() => cell.data.endTime || dayjs().format(timeFormat), [cell.data.endTime, timeFormat]); + + const viewId = useViewId(); + const { t } = useTranslation(); + + const handleChange = useCallback( + async (params: { + includeTime?: boolean; + date?: number; + endDate?: number; + time?: string; + endTime?: string; + isRange?: boolean; + clearFlag?: boolean; + }) => { + try { + const isRange = params.isRange ?? cell.data.isRange; + + await updateDateCell(viewId, cell.rowId, cell.fieldId, { + date: params.date ?? timestamp, + endDate: isRange ? params.endDate ?? endTimestamp : undefined, + time: params.time ?? time, + endTime: isRange ? params.endTime ?? endTime : undefined, + includeTime: params.includeTime ?? includeTime, + isRange, + clearFlag: params.clearFlag, + }); + } catch (e) { + // toast.error(e.message); + } + }, + [cell, endTime, endTimestamp, includeTime, time, timestamp, viewId] + ); + + const isRange = cell.data.isRange || false; + + return ( + + + + + + +
+ { + void handleChange({ + isRange: val, + // reset endTime when isRange is changed + endTime: time, + endDate: timestamp, + }); + }} + checked={isRange} + /> + { + void handleChange({ + includeTime: val, + // reset time when includeTime is changed + time: val ? dayjs().format(timeFormat) : undefined, + endTime: val && isRange ? dayjs().format(timeFormat) : undefined, + }); + }} + checked={includeTime} + /> +
+ + + + + + { + await handleChange({ + clearFlag: true, + }); + + props.onClose?.({}, 'backdropClick'); + }} + > + {t('grid.field.clearDate')} + + +
+ ); +} + +export default DateTimeCellActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFieldActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFieldActions.tsx new file mode 100644 index 0000000000..8225413a71 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFieldActions.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { UndeterminedDateField } from '$app/components/database/application'; +import DateTimeFormat from '$app/components/database/components/field_types/date/DateTimeFormat'; +import { Divider } from '@mui/material'; + +function DateTimeFieldActions({ field }: { field: UndeterminedDateField }) { + return ( + <> +
+ +
+ + + ); +} + +export default DateTimeFieldActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormat.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormat.tsx new file mode 100644 index 0000000000..7033077a00 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormat.tsx @@ -0,0 +1,75 @@ +import React, { useCallback } from 'react'; +import DateFormat from '$app/components/database/components/field_types/date/DateFormat'; +import TimeFormat from '$app/components/database/components/field_types/date/TimeFormat'; +import { TimeStampTypeOption, UndeterminedDateField, updateTypeOption } from '$app/components/database/application'; +import { DateFormatPB, FieldType, TimeFormatPB } from '@/services/backend'; +import { useViewId } from '$app/hooks'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; +import IncludeTimeSwitch from '$app/components/database/components/field_types/date/IncludeTimeSwitch'; +import { useTypeOption } from '$app/components/database'; + +interface Props { + field: UndeterminedDateField; + showLabel?: boolean; +} + +function DateTimeFormat({ field, showLabel = true }: Props) { + const viewId = useViewId(); + const { t } = useTranslation(); + const showIncludeTime = field.type === FieldType.CreatedTime || field.type === FieldType.LastEditedTime; + const typeOption = useTypeOption(field.id); + const { timeFormat = TimeFormatPB.TwentyFourHour, dateFormat = DateFormatPB.Friendly, includeTime } = typeOption; + const handleChange = useCallback( + async (params: { timeFormat?: TimeFormatPB; dateFormat?: DateFormatPB; includeTime?: boolean }) => { + try { + await updateTypeOption(viewId, field.id, field.type, { + timeFormat: params.timeFormat ?? timeFormat, + dateFormat: params.dateFormat ?? dateFormat, + includeTime: params.includeTime ?? includeTime, + fieldType: field.type, + }); + } catch (e) { + // toast.error(e.message); + } + }, + [dateFormat, field.id, field.type, includeTime, timeFormat, viewId] + ); + + return ( +
+ {showLabel && ( + + {t('grid.field.format')} + + )} + + { + void handleChange({ dateFormat: val }); + }} + /> + { + void handleChange({ timeFormat: val }); + }} + /> + + {showIncludeTime && ( +
+ { + void handleChange({ includeTime: checked }); + }} + /> +
+ )} +
+ ); +} + +export default DateTimeFormat; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormatSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormatSelect.tsx new file mode 100644 index 0000000000..3cf249602e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormatSelect.tsx @@ -0,0 +1,47 @@ +import React, { useState, useRef } from 'react'; +import { Menu, MenuItem } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { DateTimeField } from '$app/components/database/application'; +import DateTimeFormat from '$app/components/database/components/field_types/date/DateTimeFormat'; +import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; + +interface Props { + field: DateTimeField; +} + +function DateTimeFormatSelect({ field }: Props) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + return ( + <> + setOpen(true)} className={'text-xs font-medium'}> +
+ {t('grid.field.dateFormat')} & {t('grid.field.timeFormat')} +
+ +
+ setOpen(false)} + MenuListProps={{ + className: 'px-2', + }} + > + + + + ); +} + +export default DateTimeFormatSelect; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeInput.tsx new file mode 100644 index 0000000000..4114c93e4d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeInput.tsx @@ -0,0 +1,81 @@ +import React, { useMemo } from 'react'; +import { DateField, TimeField } from '@mui/x-date-pickers-pro'; +import dayjs from 'dayjs'; +import { Divider } from '@mui/material'; + +interface Props { + onChange: (params: { date?: number; time?: string }) => void; + date?: number; + time?: string; + timeFormat: string; + dateFormat: string; + includeTime?: boolean; +} + +const sx = { + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-input': { + padding: '0', + }, +}; + +function DateTimeInput({ includeTime, dateFormat, timeFormat, ...props }: Props) { + const date = useMemo(() => { + return dayjs.unix(props.date || dayjs().unix()); + }, [props.date]); + + const time = useMemo(() => { + return dayjs(dayjs().format('YYYY/MM/DD ') + props.time); + }, [props.time]); + + return ( +
+ { + if (!date) return; + props.onChange({ + date: date.unix(), + }); + }} + inputProps={{ + className: 'text-[12px]', + }} + format={dateFormat} + size={'small'} + sx={sx} + className={'flex-1 pl-2'} + /> + + {includeTime && ( + <> + + { + if (!time) return; + props.onChange({ + time: time.format(timeFormat), + }); + }} + format={timeFormat} + size={'small'} + sx={sx} + className={'w-[70px] pl-1'} + /> + + )} +
+ ); +} + +export default DateTimeInput; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeSet.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeSet.tsx new file mode 100644 index 0000000000..8e86b952d1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeSet.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { LocalizationProvider } from '@mui/x-date-pickers-pro'; +import { AdapterDayjs } from '@mui/x-date-pickers-pro/AdapterDayjs'; +import DateTimeInput from '$app/components/database/components/field_types/date/DateTimeInput'; + +interface Props { + onChange: (params: { date?: number; endDate?: number; time?: string; endTime?: string }) => void; + date?: number; + endDate?: number; + time?: string; + endTime?: string; + isRange?: boolean; + timeFormat: string; + dateFormat: string; + includeTime?: boolean; +} +function DateTimeSet({ onChange, date, endDate, time, endTime, isRange, timeFormat, dateFormat, includeTime }: Props) { + return ( +
+ + { + onChange({ + date, + time, + }); + }} + date={date} + time={time} + timeFormat={timeFormat} + dateFormat={dateFormat} + includeTime={includeTime} + /> + {isRange && ( + { + onChange({ + endDate: date, + endTime: time, + }); + }} + timeFormat={timeFormat} + dateFormat={dateFormat} + includeTime={includeTime} + /> + )} + +
+ ); +} + +export default DateTimeSet; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/IncludeTimeSwitch.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/IncludeTimeSwitch.tsx new file mode 100644 index 0000000000..f40e179ae4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/IncludeTimeSwitch.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Switch, SwitchProps } from '@mui/material'; +import { ReactComponent as TimeSvg } from '$app/assets/database/field-type-last-edited-time.svg'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +function IncludeTimeSwitch({ + checked, + onIncludeTimeChange, + ...props +}: SwitchProps & { + onIncludeTimeChange: (checked: boolean) => void; +}) { + const { t } = useTranslation(); + const handleChange = (event: React.ChangeEvent) => { + onIncludeTimeChange(event.target.checked); + }; + + return ( +
+
+ + {t('grid.field.includeTime')} +
+ +
+ ); +} + +export default IncludeTimeSwitch; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/RangeSwitch.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/RangeSwitch.tsx new file mode 100644 index 0000000000..76431af5fa --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/RangeSwitch.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Switch, SwitchProps } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import Typography from '@mui/material/Typography'; +import { ReactComponent as DateSvg } from '$app/assets/database/field-type-date.svg'; + +function RangeSwitch({ + checked, + onIsRangeChange, + ...props +}: SwitchProps & { + onIsRangeChange: (checked: boolean) => void; +}) { + const { t } = useTranslation(); + const handleChange = (event: React.ChangeEvent) => { + onIsRangeChange(event.target.checked); + }; + + return ( +
+
+ + {t('grid.field.isRange')} +
+ +
+ ); +} + +export default RangeSwitch; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/TimeFormat.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/TimeFormat.tsx new file mode 100644 index 0000000000..a575b5fcda --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/TimeFormat.tsx @@ -0,0 +1,67 @@ +import React, { useMemo, useRef, useState } from 'react'; +import { TimeFormatPB } from '@/services/backend'; +import { useTranslation } from 'react-i18next'; +import { Menu, MenuItem } from '@mui/material'; +import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg'; + +interface Props { + value: TimeFormatPB; + onChange: (value: TimeFormatPB) => void; +} +function TimeFormat({ value, onChange }: Props) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + const timeFormatMap = useMemo( + () => ({ + [TimeFormatPB.TwelveHour]: t('grid.field.timeFormatTwelveHour'), + [TimeFormatPB.TwentyFourHour]: t('grid.field.timeFormatTwentyFourHour'), + }), + [t] + ); + + const handleClick = (option: TimeFormatPB) => { + onChange(option); + setOpen(false); + }; + + return ( + <> + setOpen(true)} + > + {t('grid.field.timeFormat')} + + + setOpen(false)} + > + {Object.keys(timeFormatMap).map((option) => { + const optionValue = Number(option) as TimeFormatPB; + + return ( + handleClick(optionValue)} + > + {timeFormatMap[optionValue]} + {value === optionValue && } + + ); + })} + + + ); +} + +export default TimeFormat; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/utils.ts new file mode 100644 index 0000000000..129e84c4e7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/utils.ts @@ -0,0 +1,29 @@ +import { DateFormatPB, TimeFormatPB } from '@/services/backend'; + +export function getTimeFormat(timeFormat?: TimeFormatPB) { + switch (timeFormat) { + case TimeFormatPB.TwelveHour: + return 'h:mm A'; + case TimeFormatPB.TwentyFourHour: + return 'HH:mm'; + default: + return 'HH:mm'; + } +} + +export function getDateFormat(dateFormat?: DateFormatPB) { + switch (dateFormat) { + case DateFormatPB.Friendly: + return 'MMM DD, YYYY'; + case DateFormatPB.ISO: + return 'YYYY-MMM-DD'; + case DateFormatPB.US: + return 'YYYY/MMM/DD'; + case DateFormatPB.Local: + return 'MMM/DD/YYYY'; + case DateFormatPB.DayMonthYear: + return 'DD/MMM/YYYY'; + default: + return 'YYYY-MMM-DD'; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/EditNumberCellInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/EditNumberCellInput.tsx new file mode 100644 index 0000000000..1e15ac201c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/EditNumberCellInput.tsx @@ -0,0 +1,66 @@ +import React, { useCallback } from 'react'; +import { Popover } from '@mui/material'; +import InputBase from '@mui/material/InputBase'; + +function EditNumberCellInput({ + editing, + anchorEl, + width, + onClose, + value, + onChange, +}: { + editing: boolean; + anchorEl: HTMLDivElement | null; + width: number | undefined; + onClose: () => void; + value: string; + onChange: (value: string) => void; +}) { + const handleInput = (e: React.FormEvent) => { + const value = (e.target as HTMLInputElement).value; + + onChange(value); + }; + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + onClose(); + } + }, + [onClose] + ); + + return ( + + + + ); +} + +export default EditNumberCellInput; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFieldActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFieldActions.tsx new file mode 100644 index 0000000000..7bc654918b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFieldActions.tsx @@ -0,0 +1,34 @@ +import React, { useCallback } from 'react'; +import { NumberField, NumberTypeOption, updateTypeOption } from '$app/components/database/application'; +import { Divider } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import NumberFormatSelect from '$app/components/database/components/field_types/number/NumberFormatSelect'; +import { NumberFormatPB } from '@/services/backend'; +import { useViewId } from '$app/hooks'; +import { useTypeOption } from '$app/components/database'; + +function NumberFieldActions({ field }: { field: NumberField }) { + const viewId = useViewId(); + const { t } = useTranslation(); + const typeOption = useTypeOption(field.id); + const onChange = useCallback( + async (value: NumberFormatPB) => { + await updateTypeOption(viewId, field.id, field.type, { + format: value, + }); + }, + [field.id, field.type, viewId] + ); + + return ( + <> +
+
{t('grid.field.format')}
+ +
+ + + ); +} + +export default NumberFieldActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatMenu.tsx new file mode 100644 index 0000000000..ecaed94dc0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatMenu.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { NumberFormatPB } from '@/services/backend'; +import { Menu, MenuItem, MenuProps } from '@mui/material'; +import { formats } from '$app/components/database/components/field_types/number/const'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg'; + +function NumberFormatMenu({ + value, + onChangeFormat, + ...props +}: MenuProps & { + value: NumberFormatPB; + onChangeFormat: (value: NumberFormatPB) => void; +}) { + return ( + + {formats.map((format) => ( + { + onChangeFormat(format.value as NumberFormatPB); + props.onClose?.({}, 'backdropClick'); + }} + className={'flex justify-between text-xs font-medium'} + key={format.value} + > +
{format.key}
+ {value === format.value && } +
+ ))} +
+ ); +} + +export default NumberFormatMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatSelect.tsx new file mode 100644 index 0000000000..1e963e8d37 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatSelect.tsx @@ -0,0 +1,49 @@ +import React, { useRef, useState } from 'react'; +import { MenuItem } from '@mui/material'; +import { NumberFormatPB } from '@/services/backend'; +import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; +import { formatText } from '$app/components/database/components/field_types/number/const'; +import NumberFormatMenu from '$app/components/database/components/field_types/number/NumberFormatMenu'; + +function NumberFormatSelect({ value, onChange }: { value: NumberFormatPB; onChange: (value: NumberFormatPB) => void }) { + const ref = useRef(null); + const [expanded, setExpanded] = useState(false); + + return ( + <> + { + setExpanded(!expanded); + }} + className={'flex w-full justify-between'} + > +
{formatText(value)}
+ +
+ setExpanded(false)} + onChangeFormat={onChange} + /> + + ); +} + +export default NumberFormatSelect; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/const.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/const.ts new file mode 100644 index 0000000000..38621cb114 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/const.ts @@ -0,0 +1,14 @@ +import { NumberFormatPB } from '@/services/backend'; + +export const formats = Object.entries(NumberFormatPB) + .filter(([, value]) => typeof value !== 'string') + .map(([key, value]) => { + return { + key, + value, + }; + }); + +export const formatText = (format: NumberFormatPB) => { + return formats.find((item) => item.value === format)?.key; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionMenu.tsx new file mode 100644 index 0000000000..511a4e8486 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionMenu.tsx @@ -0,0 +1,130 @@ +import { FC, useState } from 'react'; +import { t } from 'i18next'; +import { Divider, ListSubheader, MenuItem, MenuList, MenuProps, OutlinedInput } from '@mui/material'; +import { SelectOptionColorPB } from '@/services/backend'; +import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg'; +import { SelectOption } from '../../../application'; +import { SelectOptionColorMap, SelectOptionColorTextMap } from './constants'; +import Button from '@mui/material/Button'; +import { + deleteSelectOption, + insertOrUpdateSelectOption, +} from '$app/components/database/application/field/select_option/select_option_service'; +import { useViewId } from '$app/hooks'; +import Popover from '@mui/material/Popover'; + +interface SelectOptionMenuProps { + fieldId: string; + option: SelectOption; + MenuProps: MenuProps; +} + +const Colors = [ + SelectOptionColorPB.Purple, + SelectOptionColorPB.Pink, + SelectOptionColorPB.LightPink, + SelectOptionColorPB.Orange, + SelectOptionColorPB.Yellow, + SelectOptionColorPB.Lime, + SelectOptionColorPB.Green, + SelectOptionColorPB.Aqua, + SelectOptionColorPB.Blue, +]; + +export const SelectOptionMenu: FC = ({ fieldId, option, MenuProps: menuProps }) => { + const [tagName, setTagName] = useState(option.name); + const viewId = useViewId(); + const updateColor = async (color: SelectOptionColorPB) => { + await insertOrUpdateSelectOption(viewId, fieldId, [ + { + ...option, + color, + }, + ]); + }; + + const updateName = async () => { + if (tagName === option.name) return; + await insertOrUpdateSelectOption(viewId, fieldId, [ + { + ...option, + name: tagName, + }, + ]); + }; + + const onClose = () => { + menuProps.onClose?.({}, 'backdropClick'); + }; + + const deleteOption = async () => { + await deleteSelectOption(viewId, fieldId, [option]); + onClose(); + }; + + return ( + + + { + setTagName(e.target.value); + }} + onBlur={updateName} + onKeyDown={(e) => { + if (e.key === 'Enter') { + void updateName(); + } + }} + autoFocus={true} + placeholder={t('grid.selectOption.tagName')} + size='small' + /> + +
+ +
+ + + {t('grid.selectOption.colorPanelTitle')} + + {Colors.map((color) => ( + { + void updateColor(color); + }} + key={color} + value={color} + > + + {t(`grid.selectOption.${SelectOptionColorTextMap[color]}`)} + {option.color === color && } + + ))} + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/Tag.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/Tag.tsx similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/Tag.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/Tag.tsx diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/constants.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/constants.ts similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/constants.ts rename to frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/constants.ts diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/CreateOption.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/CreateOption.tsx new file mode 100644 index 0000000000..03ca280599 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/CreateOption.tsx @@ -0,0 +1,16 @@ +import { MenuItem, MenuItemProps } from '@mui/material'; +import { FC } from 'react'; +import { Tag } from '../Tag'; + +export interface CreateOptionProps { + label: React.ReactNode; + onClick?: MenuItemProps['onClick']; +} + +export const CreateOption: FC = ({ label, onClick }) => { + return ( + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SearchInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SearchInput.tsx new file mode 100644 index 0000000000..2ac5f524ee --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SearchInput.tsx @@ -0,0 +1,41 @@ +import React, { FormEvent, useCallback } from 'react'; +import { ListSubheader, OutlinedInput } from '@mui/material'; +import { t } from 'i18next'; + +function SearchInput({ + setNewOptionName, + newOptionName, + onEnter, +}: { + newOptionName: string; + setNewOptionName: (value: string) => void; + onEnter: () => void; +}) { + const handleInput = useCallback( + (event: FormEvent) => { + const value = (event.target as HTMLInputElement).value; + + setNewOptionName(value); + }, + [setNewOptionName] + ); + + return ( + + { + if (e.key === 'Enter') { + onEnter(); + } + }} + placeholder={t('grid.selectOption.searchOrCreateOption')} + /> + + ); +} + +export default SearchInput; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectCellActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectCellActions.tsx new file mode 100644 index 0000000000..357dceab12 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectCellActions.tsx @@ -0,0 +1,152 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { MenuItem } from '@mui/material'; +import { t } from 'i18next'; +import { CreateOption } from '$app/components/database/components/field_types/select/select_cell_actions/CreateOption'; +import { SelectOptionItem } from '$app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem'; +import { + cellService, + SelectCell as SelectCellType, + SelectField, + SelectTypeOption, +} from '$app/components/database/application'; +import { useViewId } from '$app/hooks'; +import { + createSelectOption, + insertOrUpdateSelectOption, +} from '$app/components/database/application/field/select_option/select_option_service'; +import { FieldType } from '@/services/backend'; +import { useTypeOption } from '$app/components/database'; +import SearchInput from './SearchInput'; + +function SelectCellActions({ + field, + cell, + onUpdated, +}: { + field: SelectField; + cell: SelectCellType; + onUpdated?: () => void; +}) { + const rowId = cell?.rowId; + const viewId = useViewId(); + const typeOption = useTypeOption(field.id); + const options = useMemo(() => typeOption.options ?? [], [typeOption.options]); + + const selectedOptionIds = useMemo(() => cell?.data?.selectedOptionIds ?? [], [cell]); + const [newOptionName, setNewOptionName] = useState(''); + const filteredOptions = useMemo( + () => + options.filter((option) => { + return option.name.toLowerCase().includes(newOptionName.toLowerCase()); + }), + [options, newOptionName] + ); + + const shouldCreateOption = !!newOptionName && filteredOptions.length === 0; + + const updateCell = useCallback( + async (optionIds: string[]) => { + if (!cell || !rowId) return; + const prev = selectedOptionIds; + const deleteOptionIds = prev?.filter((id) => optionIds.find((cur) => cur === id) === undefined); + + await cellService.updateSelectCell(viewId, rowId, field.id, { + insertOptionIds: optionIds, + deleteOptionIds, + }); + onUpdated?.(); + }, + [cell, field.id, onUpdated, rowId, selectedOptionIds, viewId] + ); + + const createOption = useCallback(async () => { + const option = await createSelectOption(viewId, field.id, newOptionName); + + if (!option) return; + await insertOrUpdateSelectOption(viewId, field.id, [option]); + setNewOptionName(''); + return option; + }, [viewId, field.id, newOptionName]); + + const handleClickOption = useCallback( + (optionId: string) => { + if (field.type === FieldType.SingleSelect) { + void updateCell([optionId]); + return; + } + + const prev = selectedOptionIds; + let newOptionIds = []; + + if (!prev) { + newOptionIds.push(optionId); + } else { + const isSelected = prev.includes(optionId); + + if (isSelected) { + newOptionIds = prev.filter((id) => id !== optionId); + } else { + newOptionIds = [...prev, optionId]; + } + } + + void updateCell(newOptionIds); + }, + [field.type, selectedOptionIds, updateCell] + ); + + const handleNewTagClick = useCallback(async () => { + if (!cell || !rowId) return; + const option = await createOption(); + + if (!option) return; + handleClickOption(option.id); + }, [cell, createOption, handleClickOption, rowId]); + + const handleEnter = useCallback(() => { + if (shouldCreateOption) { + void handleNewTagClick(); + } else { + if (field.type === FieldType.SingleSelect) { + const firstOption = filteredOptions[0]; + + if (!firstOption) return; + + void updateCell([firstOption.id]); + } else { + void updateCell(filteredOptions.map((option) => option.id)); + } + } + + setNewOptionName(''); + }, [field.type, filteredOptions, handleNewTagClick, shouldCreateOption, updateCell]); + + return ( +
+ +
+ {shouldCreateOption ? t('grid.selectOption.createNew') : t('grid.selectOption.orSelectOne')} +
+ {shouldCreateOption ? ( + + ) : ( +
+ {filteredOptions.map((option) => ( + + { + handleClickOption(option.id); + }} + isSelected={selectedOptionIds?.includes(option.id)} + fieldId={cell?.fieldId || ''} + option={option} + /> + + ))} +
+ )} +
+ ); +} + +export default SelectCellActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem.tsx new file mode 100644 index 0000000000..82eacb06a6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem.tsx @@ -0,0 +1,57 @@ +import { FC, MouseEventHandler, useCallback, useRef, useState } from 'react'; +import { IconButton } from '@mui/material'; +import { ReactComponent as DetailsSvg } from '$app/assets/details.svg'; +import { SelectOption } from '../../../../application'; +import { SelectOptionMenu } from '../SelectOptionMenu'; +import { Tag } from '../Tag'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg'; + +export interface SelectOptionItemProps { + option: SelectOption; + fieldId: string; + isSelected?: boolean; + onClick?: () => void; +} + +export const SelectOptionItem: FC = ({ onClick, isSelected, fieldId, option }) => { + const [open, setOpen] = useState(false); + const anchorEl = useRef(null); + const [hovered, setHovered] = useState(false); + const handleClick = useCallback>((event) => { + event.stopPropagation(); + setOpen(true); + }, []); + + return ( + <> +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > +
+ +
+ {isSelected && !hovered && } + {hovered && ( + + + + )} +
+ {open && ( + setOpen(false), + }} + /> + )} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/AddAnOption.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/AddAnOption.tsx new file mode 100644 index 0000000000..25a1e3eee0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/AddAnOption.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { OutlinedInput } from '@mui/material'; +import { + createSelectOption, + insertOrUpdateSelectOption, +} from '$app/components/database/application/field/select_option/select_option_service'; +import { useViewId } from '$app/hooks'; + +function AddAnOption({ fieldId }: { fieldId: string }) { + const viewId = useViewId(); + const { t } = useTranslation(); + const [edit, setEdit] = useState(false); + const [newOptionName, setNewOptionName] = useState(''); + const exitEdit = () => { + setNewOptionName(''); + setEdit(false); + }; + + const createOption = async () => { + const option = await createSelectOption(viewId, fieldId, newOptionName); + + if (!option) return; + await insertOrUpdateSelectOption(viewId, fieldId, [option]); + setNewOptionName(''); + }; + + return edit ? ( + { + setNewOptionName(e.target.value); + }} + value={newOptionName} + onKeyDown={(e) => { + if (e.key === 'Enter') { + void createOption(); + } + }} + className={'mx-2 mb-1'} + placeholder={t('grid.selectOption.typeANewOption')} + size='small' + /> + ) : ( + + ); +} + +export default AddAnOption; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Option.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Option.tsx new file mode 100644 index 0000000000..71693276c6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Option.tsx @@ -0,0 +1,46 @@ +import React, { useRef, useState } from 'react'; +import { ReactComponent as MoreIcon } from '$app/assets/more.svg'; +import { SelectOption } from '$app/components/database/application'; +// import { ReactComponent as DragIcon } from '$app/assets/drag.svg'; + +import { SelectOptionMenu } from '$app/components/database/components/field_types/select/SelectOptionMenu'; +import Button from '@mui/material/Button'; +import { SelectOptionColorMap } from '$app/components/database/components/field_types/select/constants'; + +function Option({ option, fieldId }: { option: SelectOption; fieldId: string }) { + const [expanded, setExpanded] = useState(false); + const ref = useRef(null); + + return ( + <> + + setExpanded(false), + open: expanded, + transformOrigin: { + vertical: 'center', + horizontal: 'left', + }, + anchorOrigin: { vertical: 'center', horizontal: 'right' }, + }} + option={option} + /> + + ); +} + +export default Option; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Options.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Options.tsx new file mode 100644 index 0000000000..fea4b31475 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Options.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { SelectOption } from '$app/components/database/application'; +import Option from './Option'; + +interface Props { + options: SelectOption[]; + fieldId: string; +} +function Options({ options, fieldId }: Props) { + return ( +
+ {options.map((option) => { + return
+ ); +} + +export default Options; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/SelectFieldActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/SelectFieldActions.tsx new file mode 100644 index 0000000000..80e41f11fa --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/SelectFieldActions.tsx @@ -0,0 +1,26 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import AddAnOption from '$app/components/database/components/field_types/select/select_field_actions/AddAnOption'; +import Options from '$app/components/database/components/field_types/select/select_field_actions/Options'; +import { SelectField, SelectTypeOption } from '$app/components/database/application'; +import { Divider } from '@mui/material'; +import { useTypeOption } from '$app/components/database'; + +function SelectFieldActions({ field }: { field: SelectField }) { + const typeOption = useTypeOption(field.id); + const options = useMemo(() => typeOption.options ?? [], [typeOption.options]); + const { t } = useTranslation(); + + return ( + <> +
+
{t('grid.field.optionTitle')}
+ + +
+ + + ); +} + +export default SelectFieldActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/EditTextCellInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx similarity index 59% rename from frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/EditTextCellInput.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx index 4ff5b02204..4c79482120 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/EditTextCellInput.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React from 'react'; import { Popover, TextareaAutosize } from '@mui/material'; interface Props { @@ -10,25 +10,24 @@ interface Props { onInput: (event: React.FormEvent) => void; } function EditTextCellInput({ editing, anchorEl, width, onClose, text, onInput }: Props) { - const textareaRef = useRef(null); + const handleEnter = (e: React.KeyboardEvent) => { + const shift = e.shiftKey; - useEffect(() => { - if (textareaRef.current) { - textareaRef.current.focus(); - // set the cursor to the end of the text - const length = textareaRef.current.value.length; - - textareaRef.current.setSelectionRange(length, length); + // If shift is pressed, allow the user to enter a new line, otherwise close the popover + if (!shift && e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + onClose(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [textareaRef.current]); + }; + return ( ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/ConditionSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/ConditionSelect.tsx new file mode 100644 index 0000000000..c6cb205bd2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/ConditionSelect.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import Select from '@mui/material/Select'; +import { FormControl, MenuItem, SelectProps } from '@mui/material'; + +function ConditionSelect({ + conditions, + ...props +}: SelectProps & { + conditions: { + value: number; + text: string; + }[]; +}) { + return ( + + + + ); +} + +export default ConditionSelect; 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 200e415a2e..7e786186cb 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 @@ -1,27 +1,54 @@ -import React, { FC, useState } from 'react'; -import { Filter as FilterType, Field as FieldData, UndeterminedFilter } from '$app/components/database/application'; +import React, { FC, useMemo, useState } from 'react'; +import { + Filter as FilterType, + Field as FieldData, + UndeterminedFilter, + TextFilterData, + SelectFilterData, + NumberFilterData, + CheckboxFilterData, + ChecklistFilterData, + DateFilterData, +} from '$app/components/database/application'; import { Chip, Popover } from '@mui/material'; import { Field } from '$app/components/database/components/field'; import { ReactComponent as DropDownSvg } from '$app/assets/dropdown.svg'; -import TextFilter from '$app/components/database/components/filter/field_filter/TextFilter'; +import TextFilter from './text_filter/TextFilter'; import { FieldType } from '@/services/backend'; import FilterActions from '$app/components/database/components/filter/FilterActions'; import { updateFilter } from '$app/components/database/application/filter/filter_service'; import { useViewId } from '$app/hooks'; +import SelectFilter from './select_filter/SelectFilter'; + +import DateFilter from '$app/components/database/components/filter/date_filter/DateFilter'; +import FilterConditionSelect from '$app/components/database/components/filter/FilterConditionSelect'; interface Props { filter: FilterType; field: FieldData; } +interface FilterComponentProps { + filter: FilterType; + field: FieldData; + onChange: (data: UndeterminedFilter['data']) => void; +} + +type FilterComponent = FC; const getFilterComponent = (field: FieldData) => { switch (field.type) { case FieldType.RichText: - return TextFilter as FC<{ - filter: FilterType; - field: FieldData; - onChange: (data: UndeterminedFilter['data']) => void; - }>; + case FieldType.URL: + case FieldType.Number: + return TextFilter as FilterComponent; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return SelectFilter as FilterComponent; + + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return DateFilter as FilterComponent; default: return null; } @@ -54,6 +81,29 @@ function Filter({ filter, field }: Props) { const Component = getFilterComponent(field); + const condition = useMemo(() => { + switch (field.type) { + case FieldType.RichText: + case FieldType.URL: + return (filter.data as TextFilterData).condition; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return (filter.data as SelectFilterData).condition; + case FieldType.Number: + return (filter.data as NumberFilterData).condition; + case FieldType.Checkbox: + return (filter.data as CheckboxFilterData).condition; + case FieldType.Checklist: + return (filter.data as ChecklistFilterData).condition; + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return (filter.data as DateFilterData).condition; + default: + return; + } + }, [field, filter]); + return ( <> - -
+ {condition !== undefined && open && ( + +
+ { + void onDataChange({ + condition, + }); + }} + /> + +
{Component && } - -
-
+ + )} ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterActions.tsx index 230b97a756..f48908adbd 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterActions.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterActions.tsx @@ -29,6 +29,7 @@ function FilterActions({ filter }: { filter: Filter }) { onClick={(e) => { setAnchorEl(e.currentTarget); }} + className={'mx-2 my-1.5'} > diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterConditionSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterConditionSelect.tsx new file mode 100644 index 0000000000..ca5731222f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterConditionSelect.tsx @@ -0,0 +1,201 @@ +import React, { useMemo } from 'react'; +import ConditionSelect from './ConditionSelect'; +import { + CheckboxFilterConditionPB, + ChecklistFilterConditionPB, + DateFilterConditionPB, + FieldType, + NumberFilterConditionPB, + SelectOptionConditionPB, + TextFilterConditionPB, +} from '@/services/backend'; + +import { useTranslation } from 'react-i18next'; + +function FilterConditionSelect({ + name, + condition, + fieldType, + onChange, +}: { + name: string; + condition: number; + fieldType: FieldType; + onChange: (condition: number) => void; +}) { + const { t } = useTranslation(); + const conditions = useMemo(() => { + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return [ + { + value: TextFilterConditionPB.Contains, + text: t('grid.textFilter.contains'), + }, + { + value: TextFilterConditionPB.DoesNotContain, + text: t('grid.textFilter.doesNotContain'), + }, + { + value: TextFilterConditionPB.StartsWith, + text: t('grid.textFilter.startWith'), + }, + { + value: TextFilterConditionPB.EndsWith, + text: t('grid.textFilter.endsWith'), + }, + { + value: TextFilterConditionPB.Is, + text: t('grid.textFilter.is'), + }, + { + value: TextFilterConditionPB.IsNot, + text: t('grid.textFilter.isNot'), + }, + { + value: TextFilterConditionPB.TextIsEmpty, + text: t('grid.textFilter.isEmpty'), + }, + { + value: TextFilterConditionPB.TextIsNotEmpty, + text: t('grid.textFilter.isNotEmpty'), + }, + ]; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return [ + { + value: SelectOptionConditionPB.OptionIs, + text: t('grid.singleSelectOptionFilter.is'), + }, + { + value: SelectOptionConditionPB.OptionIsNot, + text: t('grid.singleSelectOptionFilter.isNot'), + }, + { + value: SelectOptionConditionPB.OptionIsEmpty, + text: t('grid.singleSelectOptionFilter.isEmpty'), + }, + { + value: SelectOptionConditionPB.OptionIsNotEmpty, + text: t('grid.singleSelectOptionFilter.isNotEmpty'), + }, + ]; + + case FieldType.Number: + return [ + { + value: NumberFilterConditionPB.Equal, + text: '=', + }, + { + value: NumberFilterConditionPB.NotEqual, + text: '!=', + }, + { + value: NumberFilterConditionPB.GreaterThan, + text: '>', + }, + { + value: NumberFilterConditionPB.LessThan, + text: '<', + }, + { + value: NumberFilterConditionPB.GreaterThanOrEqualTo, + text: '>=', + }, + { + value: NumberFilterConditionPB.LessThanOrEqualTo, + text: '<=', + }, + { + value: NumberFilterConditionPB.NumberIsEmpty, + text: t('grid.textFilter.isEmpty'), + }, + { + value: NumberFilterConditionPB.NumberIsNotEmpty, + text: t('grid.textFilter.isNotEmpty'), + }, + ]; + case FieldType.Checkbox: + return [ + { + value: CheckboxFilterConditionPB.IsChecked, + text: t('grid.checkboxFilter.isChecked'), + }, + { + value: CheckboxFilterConditionPB.IsUnChecked, + text: t('grid.checkboxFilter.isUnchecked'), + }, + ]; + case FieldType.Checklist: + return [ + { + value: ChecklistFilterConditionPB.IsComplete, + text: t('grid.checklistFilter.isComplete'), + }, + { + value: ChecklistFilterConditionPB.IsIncomplete, + text: t('grid.checklistFilter.isIncomplted'), + }, + ]; + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return [ + { + value: DateFilterConditionPB.DateIs, + text: t('grid.dateFilter.is'), + }, + { + value: DateFilterConditionPB.DateBefore, + text: t('grid.dateFilter.before'), + }, + { + value: DateFilterConditionPB.DateAfter, + text: t('grid.dateFilter.after'), + }, + { + value: DateFilterConditionPB.DateOnOrBefore, + text: t('grid.dateFilter.onOrBefore'), + }, + { + value: DateFilterConditionPB.DateOnOrAfter, + text: t('grid.dateFilter.onOrAfter'), + }, + { + value: DateFilterConditionPB.DateWithIn, + text: t('grid.dateFilter.between'), + }, + { + value: DateFilterConditionPB.DateIsEmpty, + text: t('grid.dateFilter.empty'), + }, + { + value: DateFilterConditionPB.DateIsNotEmpty, + text: t('grid.dateFilter.notEmpty'), + }, + ]; + default: + return []; + } + }, [fieldType, t]); + + return ( +
+
{name}
+ { + const value = Number(e.target.value); + + onChange(value); + }} + value={condition} + /> +
+ ); +} + +export default FilterConditionSelect; 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 d87037cfe9..ce7109776d 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,11 +1,12 @@ import React, { MouseEvent, useCallback } from 'react'; -import { Menu, MenuProps } from '@mui/material'; +import { MenuProps } from '@mui/material'; import FieldList from '$app/components/database/components/field/FieldList'; import { Field } from '$app/components/database/application'; import { useViewId } from '$app/hooks'; import { useTranslation } from 'react-i18next'; import { insertFilter } from '$app/components/database/application/filter/filter_service'; import { getDefaultFilter } from '$app/components/database/application/filter/filter_data'; +import Popover from '@mui/material/Popover'; function FilterFieldsMenu({ onInserted, @@ -20,10 +21,6 @@ function FilterFieldsMenu({ async (event: MouseEvent, field: Field) => { const filterData = getDefaultFilter(field.type); - if (!filterData) { - return; - } - await insertFilter({ viewId, fieldId: field.id, @@ -37,9 +34,9 @@ function FilterFieldsMenu({ ); return ( - + - + ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filters.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filters.tsx index 4de49db195..2ded60dbb4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filters.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filters.tsx @@ -1,14 +1,13 @@ import React, { useMemo, useState } from 'react'; -import { useDatabase } from '$app/components/database'; import Filter from '$app/components/database/components/filter/Filter'; import Button from '@mui/material/Button'; import FilterFieldsMenu from '$app/components/database/components/filter/FilterFieldsMenu'; import { useTranslation } from 'react-i18next'; import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { useDatabase } from '$app/components/database'; function Filters() { const { t } = useTranslation(); - const { filters, fields } = useDatabase(); const options = useMemo(() => { @@ -35,9 +34,18 @@ function Filters() { - setFilterAnchorEl(null)} /> + setFilterAnchorEl(null)} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + />
); } -export default React.memo(Filters); +export default Filters; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilter.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilter.tsx new file mode 100644 index 0000000000..dabcb42b19 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilter.tsx @@ -0,0 +1,98 @@ +import React, { useMemo } from 'react'; +import { + DateFilter as DateFilterType, + DateFilterData, + DateTimeField, + DateTimeTypeOption, +} from '$app/components/database/application'; +import { DateFilterConditionPB } from '@/services/backend'; +import CustomCalendar from '$app/components/database/components/field_types/date/CustomCalendar'; +import DateTimeSet from '$app/components/database/components/field_types/date/DateTimeSet'; +import { useTypeOption } from '$app/components/database'; +import { getDateFormat, getTimeFormat } from '$app/components/database/components/field_types/date/utils'; + +interface Props { + filter: DateFilterType; + field: DateTimeField; + onChange: (filterData: DateFilterData) => void; +} + +function DateFilter({ filter, field, onChange }: Props) { + const typeOption = useTypeOption(field.id); + + const showCalendar = + filter.data.condition !== DateFilterConditionPB.DateIsEmpty && + filter.data.condition !== DateFilterConditionPB.DateIsNotEmpty; + + const condition = filter.data.condition; + const isRange = condition === DateFilterConditionPB.DateWithIn; + const timestamp = useMemo(() => { + const now = Date.now() / 1000; + + if (isRange) { + return filter.data.start ? filter.data.start : now; + } + + return filter.data.timestamp ? filter.data.timestamp : now; + }, [filter.data.start, filter.data.timestamp, isRange]); + + const endTimestamp = useMemo(() => { + const now = Date.now() / 1000; + + if (isRange) { + return filter.data.end ? filter.data.end : now; + } + + return now; + }, [filter.data.end, isRange]); + + const timeFormat = useMemo(() => { + return getTimeFormat(typeOption.timeFormat); + }, [typeOption.timeFormat]); + + const dateFormat = useMemo(() => { + return getDateFormat(typeOption.dateFormat); + }, [typeOption.dateFormat]); + + return ( +
+ {showCalendar && ( + <> +
+ { + onChange({ + condition, + timestamp: date, + start: date, + end: endDate, + }); + }} + date={timestamp} + endDate={endTimestamp} + timeFormat={timeFormat} + dateFormat={dateFormat} + includeTime={false} + isRange={isRange} + /> +
+ { + onChange({ + condition, + timestamp: date, + start: date, + end: endDate, + }); + }} + isRange={isRange} + timestamp={timestamp} + endTimestamp={endTimestamp} + /> + + )} +
+ ); +} + +export default DateFilter; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/field_filter/TextFilter.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/field_filter/TextFilter.tsx deleted file mode 100644 index 5c238dec2a..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/field_filter/TextFilter.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { useState } from 'react'; -import { Field, TextFilter as TextFilterType, TextFilterData } from '$app/components/database/application'; -import TextFilterConditionSelect from '$app/components/database/components/filter/field_filter/TextFilterConditionSelect'; -import { TextField } from '@mui/material'; -import { useTranslation } from 'react-i18next'; - -interface Props { - filter: TextFilterType; - field: Field; - onChange: (filterData: TextFilterData) => void; -} -function TextFilter({ filter, field, onChange }: Props) { - const { t } = useTranslation(); - const [selectedCondition, setSelectedCondition] = useState(filter.data.condition); - const [content, setContext] = useState(filter.data.content); - - return ( -
-
-
{field.name}
- { - const value = Number(e.target.value); - - setSelectedCondition(value); - onChange({ - condition: value, - content, - }); - }} - value={selectedCondition} - /> -
- { - setContext(e.target.value); - }} - onBlur={() => { - onChange({ - condition: selectedCondition, - content, - }); - }} - /> -
- ); -} - -export default TextFilter; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/field_filter/TextFilterConditionSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/field_filter/TextFilterConditionSelect.tsx deleted file mode 100644 index 7f54abc429..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/field_filter/TextFilterConditionSelect.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useCallback } from 'react'; -import { MenuItem, SelectProps, FormControl } from '@mui/material'; -import Select from '@mui/material/Select'; -import { TextFilterConditionPB } from '@/services/backend'; -import { useTranslation } from 'react-i18next'; - -const TextFilterConditions = Object.values(TextFilterConditionPB).filter( - (item) => typeof item !== 'string' -) as TextFilterConditionPB[]; - -function TextFilterConditionSelect(props: SelectProps) { - const { t } = useTranslation(); - const getText = useCallback( - (type: TextFilterConditionPB) => { - switch (type) { - case TextFilterConditionPB.Contains: - return t('grid.textFilter.contains'); - case TextFilterConditionPB.DoesNotContain: - return t('grid.textFilter.doesNotContain'); - case TextFilterConditionPB.Is: - return t('grid.textFilter.is'); - case TextFilterConditionPB.IsNot: - return t('grid.textFilter.isNot'); - case TextFilterConditionPB.StartsWith: - return t('grid.textFilter.startWith'); - case TextFilterConditionPB.EndsWith: - return t('grid.textFilter.endsWith'); - case TextFilterConditionPB.TextIsEmpty: - return t('grid.textFilter.isEmpty'); - case TextFilterConditionPB.TextIsNotEmpty: - return t('grid.textFilter.isNotEmpty'); - default: - return ''; - } - }, - [t] - ); - - return ( - - - - ); -} - -export default TextFilterConditionSelect; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilter.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilter.tsx new file mode 100644 index 0000000000..064e4e4047 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilter.tsx @@ -0,0 +1,87 @@ +import React, { useMemo } from 'react'; +import { + SelectField, + SelectFilter as SelectFilterType, + SelectFilterData, + SelectTypeOption, +} from '$app/components/database/application'; +import { MenuItem, MenuList } from '@mui/material'; +import { Tag } from '$app/components/database/components/field_types/select/Tag'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg'; +import { SelectOptionConditionPB } from '@/services/backend'; +import { useTypeOption } from '$app/components/database'; + +interface Props { + filter: SelectFilterType; + field: SelectField; + onChange: (filterData: SelectFilterData) => void; +} + +function SelectFilter({ filter, field, onChange }: Props) { + const condition = filter.data.condition; + const typeOption = useTypeOption(field.id); + const options = useMemo(() => typeOption.options ?? [], [typeOption]); + + const showOptions = + options.length > 0 && + condition !== SelectOptionConditionPB.OptionIsEmpty && + condition !== SelectOptionConditionPB.OptionIsNotEmpty; + + const handleChange = ({ + condition, + optionIds, + }: { + condition?: SelectFilterData['condition']; + optionIds?: SelectFilterData['optionIds']; + }) => { + onChange({ + condition: condition ?? filter.data.condition, + optionIds: optionIds ?? filter.data.optionIds, + }); + }; + + const handleSelectOption = (optionId: string) => { + const prev = filter.data.optionIds; + let newOptionIds = []; + + if (!prev) { + newOptionIds.push(optionId); + } else { + const isSelected = prev.includes(optionId); + + if (isSelected) { + newOptionIds = prev.filter((id) => id !== optionId); + } else { + newOptionIds = [...prev, optionId]; + } + } + + handleChange({ + condition, + optionIds: newOptionIds, + }); + }; + + if (!showOptions) return null; + + return ( + + {options?.map((option) => { + const isSelected = filter.data.optionIds?.includes(option.id); + + return ( + handleSelectOption(option.id)} + key={option.id} + > + + {isSelected && } + + ); + })} + + ); +} + +export default SelectFilter; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilter.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilter.tsx new file mode 100644 index 0000000000..85075d6ea6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilter.tsx @@ -0,0 +1,38 @@ +import React, { useState } from 'react'; +import { TextFilter as TextFilterType, TextFilterData } from '$app/components/database/application'; +import { TextField } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { TextFilterConditionPB } from '@/services/backend'; + +interface Props { + filter: TextFilterType; + onChange: (filterData: TextFilterData) => void; +} +function TextFilter({ filter, onChange }: Props) { + const { t } = useTranslation(); + const [content, setContext] = useState(filter.data.content); + const condition = filter.data.condition; + const showField = + condition !== TextFilterConditionPB.TextIsEmpty && condition !== TextFilterConditionPB.TextIsNotEmpty; + + if (!showField) return null; + return ( + { + setContext(e.target.value); + }} + onBlur={() => { + onChange({ + content, + condition, + }); + }} + /> + ); +} + +export default TextFilter; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortConditionSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortConditionSelect.tsx index 3618bacb55..0aea34d5f8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortConditionSelect.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortConditionSelect.tsx @@ -6,12 +6,8 @@ import { SortConditionPB } from '@/services/backend'; export const SortConditionSelect: FC> = (props) => { 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 a7b1d4e621..cc4f28823d 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,10 +1,11 @@ import React, { FC, MouseEvent, useCallback } from 'react'; -import { Menu, MenuProps } from '@mui/material'; +import { MenuProps } from '@mui/material'; import FieldList from '$app/components/database/components/field/FieldList'; import { Field, sortService } from '$app/components/database/application'; import { SortConditionPB } from '@/services/backend'; import { useTranslation } from 'react-i18next'; import { useViewId } from '$app/hooks'; +import Popover from '@mui/material/Popover'; const SortFieldsMenu: FC< MenuProps & { @@ -27,9 +28,9 @@ 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 477b169cff..63b9b68ae5 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 @@ -43,7 +43,7 @@ export const SortItem: FC = ({ className, sort }) => { }, [viewId, sort]); return ( - + = ({ className, sort }) => { onChange={handleConditionChange} />
- +
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortMenu.tsx index 23a6fe5d3a..d39f758941 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortMenu.tsx @@ -1,4 +1,4 @@ -import { Menu, MenuItem, MenuProps } from '@mui/material'; +import { Menu, MenuProps } from '@mui/material'; import { FC, MouseEventHandler, useCallback, useState } from 'react'; import { useViewId } from '$app/hooks'; import { sortService } from '../../application'; @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'; import { ReactComponent as AddSvg } from '$app/assets/add.svg'; import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg'; import SortFieldsMenu from '$app/components/database/components/sort/SortFieldsMenu'; +import Button from '@mui/material/Button'; export const SortMenu: FC = (props) => { const { onClose } = props; @@ -28,22 +29,41 @@ export const SortMenu: FC = (props) => { return ( <> - -
-
+ +
+
{sorts.map((sort) => ( ))}
- - - {t('grid.sort.addSort')} - - - - {t('grid.sort.deleteAllSorts')} - +
+ + +
@@ -53,6 +73,10 @@ export const SortMenu: FC = (props) => { onClose={() => { setAnchorEl(null); }} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} /> ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewTabs.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewTabs.tsx index fea626a2fc..86e5889a88 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewTabs.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewTabs.tsx @@ -29,16 +29,18 @@ interface TabPanelProps { export function TabPanel(props: TabPanelProps) { const { children, value, index, ...other } = props; + const isActivated = value === index; + 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 13a3506b96..829c9d855f 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 @@ -19,7 +19,7 @@ function GridCalculate({ field, index }: Props) { width, visibility: index === 0 ? 'visible' : 'hidden', }} - className={'flex justify-end'} + className={'flex justify-end py-2'} > Count {count} 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 336e73e72d..b092798bd3 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 @@ -17,17 +17,17 @@ export interface GridFieldProps { export const GridField: FC = ({ field }) => { const viewId = useViewId(); const { fields } = useDatabase(); - const [openMenu, setOpenMenu] = useState(false); + 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 handleClick = useCallback(() => { - setOpenMenu(true); + const openMenu = Boolean(menuAnchorEl); + const handleClick = useCallback((e: React.MouseEvent) => { + setMenuAnchorEl(e.currentTarget); }, []); const handleMenuClose = useCallback(() => { - setOpenMenu(false); + setMenuAnchorEl(null); }, []); const handleTooltipOpen = useCallback(() => { @@ -115,7 +115,7 @@ export const GridField: FC = ({ field }) => { onContextMenu={(event) => { event.stopPropagation(); event.preventDefault(); - handleClick(); + handleClick(event); }} onClick={handleClick} {...attributes} @@ -134,7 +134,7 @@ export const GridField: FC = ({ field }) => { setFieldWidth(width)} /> - {openMenu && } + {openMenu && }
); }; 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 index 875763f6e9..98acab36f2 100644 --- 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 @@ -111,7 +111,7 @@ export const GridCellRow: FC = ({ rowMeta, virtualizer, getPre ref={ref} className='relative -ml-16 flex grow pl-16' onMouseLeave={onMouseLeave} - onMouseEnter={onMouseEnter} + onMouseMove={onMouseEnter} {...dropListeners} >
{ if (row.type === RenderRowType.Row) { @@ -17,11 +18,12 @@ export const GridTable: FC<{ tableHeight: number }> = React.memo(({ tableHeight const verticalScrollElementRef = useRef(null); const horizontalScrollElementRef = useRef(null); const rowMetas = useDatabaseVisibilityRows(); - const renderRows = useMemo(() => rowMetasToRenderRow(rowMetas as RowMeta[]), [rowMetas]); const fields = useDatabaseVisibilityFields(); + const renderRows = useMemo(() => rowMetasToRenderRow(rowMetas as RowMeta[]), [rowMetas]); + const rowVirtualizer = useVirtualizer({ count: renderRows.length, - overscan: 5, + overscan: 10, getItemKey: (i) => getRenderRowKey(renderRows[i]), getScrollElement: () => verticalScrollElementRef.current, estimateSize: () => 37, @@ -38,25 +40,33 @@ export const GridTable: FC<{ tableHeight: number }> = React.memo(({ tableHeight }, }); - const getPrevRowId = (id: string) => { - const index = rowMetas.findIndex((rowMeta) => rowMeta.id === id); + const getPrevRowId = useCallback( + (id: string) => { + const index = rowMetas.findIndex((rowMeta) => rowMeta.id === id); - if (index === 0) { - return null; - } + if (index === 0) { + return null; + } - return rowMetas[index - 1].id; - }; + return rowMetas[index - 1].id; + }, + [rowMetas] + ); return (
+ {fields.length === 0 && ( +
+ +
+ )}
{ verticalScrollElementRef.current = e; horizontalScrollElementRef.current = e; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Breadcrumb/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Breadcrumb/index.tsx index 2eceb2fc14..91fc080463 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Breadcrumb/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Breadcrumb/index.tsx @@ -13,7 +13,7 @@ function Breadcrumb() { const { pagePath } = useLoadExpandedPages(); const navigate = useNavigate(); const activePage = useMemo(() => pagePath[pagePath.length - 1], [pagePath]); - const parentPages = useMemo(() => pagePath.slice(0, pagePath.length - 1) as Page[], [pagePath]); + const parentPages = useMemo(() => pagePath.slice(1, pagePath.length - 1) as Page[], [pagePath]); const navigateToPage = useCallback( (page: Page) => { const pageType = pageTypeMap[page.layout]; @@ -34,7 +34,7 @@ function Breadcrumb() { navigateToPage(page); }} > - {page.name} + {page.name || t('document.title.placeholder')} ))} {activePage?.name || t('menuAppHeader.defaultNewPageName')} diff --git a/frontend/appflowy_tauri/src/styles/template.css b/frontend/appflowy_tauri/src/styles/template.css index 8e39e5e2fd..d4c82aa0ab 100644 --- a/frontend/appflowy_tauri/src/styles/template.css +++ b/frontend/appflowy_tauri/src/styles/template.css @@ -74,3 +74,78 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { color: var(--text-title) !important; box-shadow: var(--line-border) 0px 0px 0px 1px inset !important; } + +.appflowy-date-picker-calendar { + width: 100%; + +} + +.react-datepicker__month-container { + width: 100%; + border-radius: 0; +} +.react-datepicker__header { + border-radius: 0; + background: transparent; + border-bottom: 0; + +} +.react-datepicker__day-names { + border: none; +} +.react-datepicker__day-name { + color: var(--text-caption); +} +.react-datepicker__month { + border: none; +} + +.react-datepicker__day { + border: none; + color: var(--text-title); + border-radius: 100%; +} +.react-datepicker__day:hover { + border-radius: 100%; + background: var(--fill-hover); +} +.react-datepicker__day--outside-month { + color: var(--text-caption); +} +.react-datepicker__day--in-range { + background: var(--fill-active); +} + +.react-datepicker__day--in-selecting-range { + background: var(--fill-active) !important; +} + + + +.react-datepicker__day--today { + border: 1px solid var(--fill-hover); + color: var(--text-title); + border-radius: 100%; + background: transparent; + font-weight: 500; + +} + +.react-datepicker__day--today:hover{ + background: var(--fill-hover); +} + +.react-datepicker__day--keyboard-selected { + background: transparent; +} + + +.react-datepicker__day--range-start, .react-datepicker__day--range-end, .react-datepicker__day--selected { + background: var(--fill-default); + color: var(--content-on-fill); +} + +.react-datepicker__day--range-start, .react-datepicker__day--range-end, .react-datepicker__day--selected:hover { + background: var(--fill-hover); +} + diff --git a/frontend/appflowy_tauri/style-dictionary/tailwind/colors.cjs b/frontend/appflowy_tauri/style-dictionary/tailwind/colors.cjs index 1cbd6015ac..54c975af66 100644 --- a/frontend/appflowy_tauri/style-dictionary/tailwind/colors.cjs +++ b/frontend/appflowy_tauri/style-dictionary/tailwind/colors.cjs @@ -65,6 +65,7 @@ module.exports = { "yellow": "var(--tint-yellow)", "pink": "var(--tint-pink)", "lime": "var(--tint-lime)", - "aqua": "var(--tint-aqua)" + "aqua": "var(--tint-aqua)", + "orange": "var(--tint-orange)", } } \ No newline at end of file diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 1d94be2fb3..c25bc209d6 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -492,6 +492,16 @@ "isEmpty": "Is empty", "isNotEmpty": "Is not empty" }, + "dateFilter": { + "is": "Is", + "before": "Is before", + "after": "Is after", + "onOrBefore": "Is on or before", + "onOrAfter": "Is on or after", + "between": "Is between", + "empty": "Is empty", + "notEmpty": "Is not empty" + }, "field": { "hide": "Hide", "show": "Show", @@ -538,20 +548,21 @@ "editProperty": "Edit property", "newProperty": "New property", "deleteFieldPromptMessage": "Are you sure? This property will be deleted", - "newColumn": "New Column" + "newColumn": "New Column", + "format": "Format" }, "rowPage": { "newField": "Add a new field", "fieldDragElementTooltip": "Click to open menu", "showHiddenFields": { - "one": "Show {} hidden field", - "many": "Show {} hidden fields", - "other": "Show {} hidden fields" + "one": "Show {count} hidden field", + "many": "Show {count} hidden fields", + "other": "Show {count} hidden fields" }, "hideHiddenFields": { - "one": "Hide {} hidden field", - "many": "Hide {} hidden fields", - "other": "Hide {} hidden fields" + "one": "Hide {count} hidden field", + "many": "Hide {count} hidden fields", + "other": "Hide {count} hidden fields" } }, "sort": { @@ -592,7 +603,9 @@ "searchOption": "Search for an option", "searchOrCreateOption": "Search or create an option...", "createNew": "Create an new", - "orSelectOne": "Or select an option" + "orSelectOne": "Or select an option", + "typeANewOption": "Type a new option", + "tagName": "Tag name" }, "checklist": { "taskHint": "Task description", @@ -864,8 +877,8 @@ "noDateTitle": "No Date", "noDateHint": { "zero": "Unscheduled events will show up here", - "one": "{} unscheduled event", - "other": "{} unscheduled events" + "one": "{count} unscheduled event", + "other": "{count} unscheduled events" }, "unscheduledEventsTitle": "Unscheduled events", "clickToAdd": "Click to add to the calendar", diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 0c9b22d230..0d7a09323c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -833,6 +833,8 @@ impl DatabaseEditor { for option in options { type_option.delete_option(&option.id); } + + notify_did_update_database_field(&self.database, field_id)?; self .database .lock()