From c1e7e7215444111838c80dd55a11d36c77c85568 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Fri, 17 May 2024 18:23:29 +0800 Subject: [PATCH] feat: support web grid preview (#5353) --- .../style-dictionary/tokens/base.json | 2 +- .../cypress/fixtures/user_workspace.json | 61 ++ .../cypress/support/commands.ts | 1 + frontend/appflowy_web_app/index.html | 4 +- frontend/appflowy_web_app/package.json | 10 +- frontend/appflowy_web_app/pnpm-lock.yaml | 84 ++- .../src/application/collab.type.ts | 269 +++++++- .../src/application/database-yjs/const.ts | 2 + .../src/application/database-yjs/context.ts | 127 ++++ .../application/database-yjs/database.type.ts | 51 ++ .../fields/checkbox/checkbox.type.ts | 10 + .../database-yjs/fields/checkbox/index.ts | 1 + .../fields/checklist/checklist.type.ts | 10 + .../database-yjs/fields/checklist/index.ts | 2 + .../database-yjs/fields/checklist/parse.ts | 22 + .../database-yjs/fields/date/date.type.ts | 32 + .../database-yjs/fields/date/index.ts | 2 + .../database-yjs/fields/date/utils.ts | 29 + .../application/database-yjs/fields/index.ts | 8 + .../fields/number/__tests__/format.test.ts | 628 ++++++++++++++++++ .../database-yjs/fields/number/format.ts | 229 +++++++ .../database-yjs/fields/number/index.ts | 3 + .../database-yjs/fields/number/number.type.ts | 56 ++ .../database-yjs/fields/number/parse.ts | 11 + .../database-yjs/fields/relation/index.ts | 2 + .../database-yjs/fields/relation/parse.ts | 9 + .../fields/relation/relation.type.ts | 9 + .../fields/select-option/index.ts | 2 + .../fields/select-option/parse.ts | 28 + .../select-option/select_option.type.ts | 38 ++ .../database-yjs/fields/text/index.ts | 1 + .../database-yjs/fields/text/text.type.ts | 17 + .../database-yjs/fields/type_option.ts | 8 + .../src/application/database-yjs/filter.ts | 223 +++++++ .../src/application/database-yjs/index.ts | 8 + .../src/application/database-yjs/selector.ts | 227 +++++++ .../src/application/database-yjs/sort.ts | 79 +++ .../src/application/document.type.ts | 176 ----- .../services/js-services/database.service.ts | 170 +++++ .../services/js-services/db/index.ts | 46 +- .../services/js-services/db/tables/users.ts | 10 - .../services/js-services/document.service.ts | 31 +- .../services/js-services/folder.service.ts | 32 +- .../application/services/js-services/index.ts | 5 + .../services/js-services/storage/auth.ts | 10 +- .../services/js-services/storage/collab.ts | 101 +++ .../services/js-services/storage/document.ts | 21 - .../services/js-services/storage/folder.ts | 21 - .../services/js-services/storage/index.ts | 6 +- .../services/js-services/storage/user.ts | 36 +- .../services/js-services/user.service.ts | 18 +- .../services/js-services/wasm/client_api.ts | 44 +- .../src/application/services/services.type.ts | 19 + .../tauri-services/database.service.ts | 29 + .../tauri-services/document.service.ts | 2 +- .../services/tauri-services/index.ts | 5 + .../application/slate-yjs/plugins/withYjs.ts | 14 +- .../slate-yjs/utils/applySlateOpts.ts | 6 +- .../utils/translateYjsEvent/textEvent.ts | 2 +- .../src/application/user.type.ts | 7 + frontend/appflowy_web_app/src/assets/add.svg | 3 - .../src/assets/align-center.svg | 5 - .../src/assets/align-left.svg | 5 - .../src/assets/align-right.svg | 5 - .../src/assets/arrow-left.svg | 3 - .../src/assets/arrow-right.svg | 3 - .../appflowy_web_app/src/assets/board.svg | 16 - frontend/appflowy_web_app/src/assets/bold.svg | 3 - .../src/assets/clock_alarm.svg | 6 - .../appflowy_web_app/src/assets/close.svg | 4 - frontend/appflowy_web_app/src/assets/copy.svg | 4 - .../appflowy_web_app/src/assets/dark-logo.svg | 73 -- .../src/assets/database/checkbox-check.svg | 4 - .../src/assets/database/checkbox-uncheck.svg | 3 - .../src/assets/database/field-type-attach.svg | 3 - .../assets/database/field-type-checkbox.svg | 4 - .../assets/database/field-type-checklist.svg | 4 - .../src/assets/database/field-type-date.svg | 6 - .../database/field-type-last-edited-time.svg | 4 - .../database/field-type-multi-select.svg | 8 - .../src/assets/database/field-type-number.svg | 3 - .../src/assets/database/field-type-person.svg | 4 - .../assets/database/field-type-relation.svg | 8 - .../database/field-type-single-select.svg | 4 - .../src/assets/database/field-type-text.svg | 4 - .../src/assets/database/field-type-url.svg | 3 - frontend/appflowy_web_app/src/assets/date.svg | 6 - .../appflowy_web_app/src/assets/delete.svg | 6 - .../appflowy_web_app/src/assets/details.svg | 5 - .../appflowy_web_app/src/assets/document.svg | 14 - frontend/appflowy_web_app/src/assets/drag.svg | 8 - .../appflowy_web_app/src/assets/dropdown.svg | 6 - frontend/appflowy_web_app/src/assets/edit.svg | 9 - .../appflowy_web_app/src/assets/eye_close.svg | 9 - .../appflowy_web_app/src/assets/eye_open.svg | 16 - frontend/appflowy_web_app/src/assets/grid.svg | 6 - frontend/appflowy_web_app/src/assets/h1.svg | 4 - frontend/appflowy_web_app/src/assets/h2.svg | 4 - frontend/appflowy_web_app/src/assets/h3.svg | 4 - .../appflowy_web_app/src/assets/hide-menu.svg | 6 - frontend/appflowy_web_app/src/assets/hide.svg | 4 - .../appflowy_web_app/src/assets/image.svg | 5 - .../src/assets/images/default_cover.jpg | Bin 281498 -> 0 bytes .../src/assets/inline-code.svg | 4 - .../appflowy_web_app/src/assets/italic.svg | 3 - frontend/appflowy_web_app/src/assets/left.svg | 5 - .../src/assets/light-logo.svg | 51 -- frontend/appflowy_web_app/src/assets/link.svg | 4 - .../src/assets/list-dropdown.svg | 4 - frontend/appflowy_web_app/src/assets/list.svg | 8 - .../appflowy_web_app/src/assets/mention.svg | 3 - frontend/appflowy_web_app/src/assets/more.svg | 3 - .../appflowy_web_app/src/assets/numbers.svg | 3 - frontend/appflowy_web_app/src/assets/open.svg | 6 - .../appflowy_web_app/src/assets/quote.svg | 4 - .../appflowy_web_app/src/assets/react.svg | 1 - .../appflowy_web_app/src/assets/right.svg | 5 - .../appflowy_web_app/src/assets/search.svg | 4 - .../src/assets/select-check.svg | 3 - .../appflowy_web_app/src/assets/settings.svg | 4 - .../src/assets/settings/account.svg | 3 - .../src/assets/settings/check_circle.svg | 8 - .../src/assets/settings/dark.png | Bin 16280 -> 0 bytes .../src/assets/settings/light.png | Bin 13240 -> 0 bytes .../src/assets/settings/workplace.svg | 10 - .../appflowy_web_app/src/assets/show-menu.svg | 6 - frontend/appflowy_web_app/src/assets/sort.svg | 4 - .../src/assets/strikethrough.svg | 4 - frontend/appflowy_web_app/src/assets/text.svg | 4 - .../appflowy_web_app/src/assets/todo-list.svg | 4 - .../appflowy_web_app/src/assets/underline.svg | 4 - frontend/appflowy_web_app/src/assets/up.svg | 3 - .../_shared/not-found/RecordNotFound.tsx | 2 +- .../components/_shared/page/usePageInfo.tsx | 8 +- .../components/_shared/popover/Popover.tsx | 19 + .../src/components/_shared/popover/index.ts | 1 + .../progress/LinearProgressWithLabel.tsx | 47 ++ .../_shared/scroller/AFScroller.tsx | 98 +-- .../src/components/_shared/tag/Tag.tsx | 29 + .../src/components/_shared/tag/index.ts | 1 + .../src/components/app/App.tsx | 2 +- .../src/components/app/AppTheme.tsx | 9 + .../src/components/auth/Welcome.cy.tsx | 3 +- .../src/components/auth/auth.hooks.ts | 1 + .../src/components/database/Database.tsx | 148 +++++ .../components/database/DatabaseContext.tsx | 10 + .../src/components/database/DatabaseTitle.tsx | 19 + .../src/components/database/board/Board.tsx | 7 + .../src/components/database/board/index.ts | 1 + .../components/database/calendar/Calendar.tsx | 7 + .../src/components/database/calendar/index.ts | 1 + .../calculation-cell/CalculationCell.tsx | 40 ++ .../components/calculation-cell/cell.type.ts | 8 + .../components/calculation-cell/index.ts | 1 + .../database/components/cell/Cell.hooks.ts | 47 ++ .../database/components/cell/Cell.tsx | 62 ++ .../database/components/cell/CheckboxCell.tsx | 14 + .../components/cell/ChecklistCell.tsx | 21 + .../database/components/cell/DateTimeCell.tsx | 35 + .../database/components/cell/NumberCell.tsx | 27 + .../database/components/cell/RelationCell.tsx | 84 +++ .../components/cell/RowCreateModifiedTime.tsx | 43 ++ .../components/cell/SelectionCell.tsx | 32 + .../database/components/cell/TextCell.tsx | 12 + .../database/components/cell/UrlCell.tsx | 37 ++ .../database/components/cell/cell.const.ts | 25 + .../database/components/cell/cell.parse.ts | 46 ++ .../database/components/cell/cell.type.ts | 90 +++ .../database/components/cell/index.ts | 1 + .../components/conditions/DatabaseActions.tsx | 35 + .../conditions/DatabaseConditions.tsx | 33 + .../database/components/conditions/context.ts | 12 + .../database/components/conditions/index.ts | 2 + .../components/field/FieldDisplay.tsx | 20 + .../components/field/FieldTypeIcon.tsx | 33 + .../database/components/field/index.ts | 2 + .../field/select-option/SelectOptionList.tsx | 30 + .../components/field/select-option/index.ts | 1 + .../database/components/filters/Filter.tsx | 57 ++ .../database/components/filters/Filters.tsx | 32 + .../filter-menu/CheckboxFilterMenu.tsx | 33 + .../filter-menu/ChecklistFilterMenu.tsx | 33 + .../filters/filter-menu/FieldMenuTitle.tsx | 23 + .../filters/filter-menu/FilterMenu.tsx | 39 ++ .../MultiSelectOptionFilterMenu.tsx | 56 ++ .../filters/filter-menu/NumberFilterMenu.tsx | 74 +++ .../SingleSelectOptionFilterMenu.tsx | 48 ++ .../filters/filter-menu/TextFilterMenu.tsx | 74 +++ .../components/filters/filter-menu/index.ts | 1 + .../database/components/filters/index.ts | 1 + .../overview/DateFilterContentOverview.tsx | 51 ++ .../overview/FilterContentOverview.tsx | 59 ++ .../overview/NumberFilterContentOverview.tsx | 38 ++ .../overview/SelectFilterContentOverview.tsx | 42 ++ .../overview/TextFilterContentOverview.tsx | 33 + .../components/filters/overview/index.ts | 1 + .../database/components/filters/package.json | 14 + .../components/grid-cell/GridCell.tsx | 64 ++ .../database/components/grid-cell/index.ts | 1 + .../components/grid-column/GridColumn.tsx | 35 + .../database/components/grid-column/index.ts | 2 + .../grid-column/useRenderColumns.tsx | 73 ++ .../components/grid-header/GridHeader.tsx | 73 ++ .../database/components/grid-header/index.ts | 1 + .../grid-row/GridCalculateRowCell.tsx | 41 ++ .../components/grid-row/GridRowCell.tsx | 28 + .../database/components/grid-row/index.ts | 3 + .../components/grid-row/useRenderRows.tsx | 44 ++ .../components/grid-table/GridTable.tsx | 177 +++++ .../database/components/grid-table/index.ts | 1 + .../database/components/sorts/Sort.tsx | 20 + .../components/sorts/SortCondition.tsx | 30 + .../database/components/sorts/SortList.tsx | 17 + .../database/components/sorts/Sorts.tsx | 43 ++ .../database/components/sorts/index.ts | 1 + .../database/components/tabs/DatabaseTabs.tsx | 97 +++ .../database/components/tabs/TextButton.tsx | 18 + .../database/components/tabs/ViewTabs.tsx | 52 ++ .../database/components/tabs/index.ts | 2 + .../src/components/database/grid/Grid.tsx | 45 ++ .../src/components/database/grid/index.ts | 1 + .../src/components/database/index.ts | 3 + .../components/editor/CollaborativeEditor.tsx | 11 +- .../src/components/editor/command/index.ts | 5 +- .../components/blocks/image/ImageEmpty.tsx | 2 +- .../blocks/todo_list/CheckboxIcon.tsx | 4 +- .../blocks/toggle_list/ToggleIcon.tsx | 2 +- .../components/leaf/mention/MentionDate.tsx | 6 +- .../src/components/error/ErrorModal.tsx | 12 +- .../src/components/layout/layout.scss | 11 +- .../src/pages/DatabasePage.tsx | 10 + .../src/pages/ProductPage.tsx | 24 +- .../src/styles/variables/dark.variables.css | 4 +- .../src/styles/variables/light.variables.css | 10 +- frontend/appflowy_web_app/src/utils/time.ts | 8 +- frontend/appflowy_web_app/src/utils/url.ts | 8 +- .../style-dictionary/tailwind/box-shadow.cjs | 2 +- .../style-dictionary/tailwind/colors.cjs | 2 +- .../style-dictionary/tokens/base.json | 2 +- frontend/appflowy_web_app/tsconfig.json | 5 +- frontend/appflowy_web_app/vite.config.ts | 7 +- 241 files changed, 5570 insertions(+), 937 deletions(-) create mode 100644 frontend/appflowy_web_app/cypress/fixtures/user_workspace.json create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/const.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/context.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/database.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/filter.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/selector.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/sort.ts delete mode 100644 frontend/appflowy_web_app/src/application/document.type.ts create mode 100644 frontend/appflowy_web_app/src/application/services/js-services/database.service.ts delete mode 100644 frontend/appflowy_web_app/src/application/services/js-services/db/tables/users.ts create mode 100644 frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts delete mode 100644 frontend/appflowy_web_app/src/application/services/js-services/storage/document.ts delete mode 100644 frontend/appflowy_web_app/src/application/services/js-services/storage/folder.ts create mode 100644 frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts delete mode 100644 frontend/appflowy_web_app/src/assets/add.svg delete mode 100644 frontend/appflowy_web_app/src/assets/align-center.svg delete mode 100644 frontend/appflowy_web_app/src/assets/align-left.svg delete mode 100644 frontend/appflowy_web_app/src/assets/align-right.svg delete mode 100644 frontend/appflowy_web_app/src/assets/arrow-left.svg delete mode 100644 frontend/appflowy_web_app/src/assets/arrow-right.svg delete mode 100644 frontend/appflowy_web_app/src/assets/board.svg delete mode 100644 frontend/appflowy_web_app/src/assets/bold.svg delete mode 100644 frontend/appflowy_web_app/src/assets/clock_alarm.svg delete mode 100644 frontend/appflowy_web_app/src/assets/close.svg delete mode 100644 frontend/appflowy_web_app/src/assets/copy.svg delete mode 100644 frontend/appflowy_web_app/src/assets/dark-logo.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/checkbox-check.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-attach.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-date.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-number.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-person.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-relation.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-text.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-url.svg delete mode 100644 frontend/appflowy_web_app/src/assets/date.svg delete mode 100644 frontend/appflowy_web_app/src/assets/delete.svg delete mode 100644 frontend/appflowy_web_app/src/assets/details.svg delete mode 100644 frontend/appflowy_web_app/src/assets/document.svg delete mode 100644 frontend/appflowy_web_app/src/assets/drag.svg delete mode 100644 frontend/appflowy_web_app/src/assets/dropdown.svg delete mode 100644 frontend/appflowy_web_app/src/assets/edit.svg delete mode 100644 frontend/appflowy_web_app/src/assets/eye_close.svg delete mode 100644 frontend/appflowy_web_app/src/assets/eye_open.svg delete mode 100644 frontend/appflowy_web_app/src/assets/grid.svg delete mode 100644 frontend/appflowy_web_app/src/assets/h1.svg delete mode 100644 frontend/appflowy_web_app/src/assets/h2.svg delete mode 100644 frontend/appflowy_web_app/src/assets/h3.svg delete mode 100644 frontend/appflowy_web_app/src/assets/hide-menu.svg delete mode 100644 frontend/appflowy_web_app/src/assets/hide.svg delete mode 100644 frontend/appflowy_web_app/src/assets/image.svg delete mode 100644 frontend/appflowy_web_app/src/assets/images/default_cover.jpg delete mode 100644 frontend/appflowy_web_app/src/assets/inline-code.svg delete mode 100644 frontend/appflowy_web_app/src/assets/italic.svg delete mode 100644 frontend/appflowy_web_app/src/assets/left.svg delete mode 100644 frontend/appflowy_web_app/src/assets/light-logo.svg delete mode 100644 frontend/appflowy_web_app/src/assets/link.svg delete mode 100644 frontend/appflowy_web_app/src/assets/list-dropdown.svg delete mode 100644 frontend/appflowy_web_app/src/assets/list.svg delete mode 100644 frontend/appflowy_web_app/src/assets/mention.svg delete mode 100644 frontend/appflowy_web_app/src/assets/more.svg delete mode 100644 frontend/appflowy_web_app/src/assets/numbers.svg delete mode 100644 frontend/appflowy_web_app/src/assets/open.svg delete mode 100644 frontend/appflowy_web_app/src/assets/quote.svg delete mode 100644 frontend/appflowy_web_app/src/assets/react.svg delete mode 100644 frontend/appflowy_web_app/src/assets/right.svg delete mode 100644 frontend/appflowy_web_app/src/assets/search.svg delete mode 100644 frontend/appflowy_web_app/src/assets/select-check.svg delete mode 100644 frontend/appflowy_web_app/src/assets/settings.svg delete mode 100644 frontend/appflowy_web_app/src/assets/settings/account.svg delete mode 100644 frontend/appflowy_web_app/src/assets/settings/check_circle.svg delete mode 100644 frontend/appflowy_web_app/src/assets/settings/dark.png delete mode 100644 frontend/appflowy_web_app/src/assets/settings/light.png delete mode 100644 frontend/appflowy_web_app/src/assets/settings/workplace.svg delete mode 100644 frontend/appflowy_web_app/src/assets/show-menu.svg delete mode 100644 frontend/appflowy_web_app/src/assets/sort.svg delete mode 100644 frontend/appflowy_web_app/src/assets/strikethrough.svg delete mode 100644 frontend/appflowy_web_app/src/assets/text.svg delete mode 100644 frontend/appflowy_web_app/src/assets/todo-list.svg delete mode 100644 frontend/appflowy_web_app/src/assets/underline.svg delete mode 100644 frontend/appflowy_web_app/src/assets/up.svg create mode 100644 frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx create mode 100644 frontend/appflowy_web_app/src/components/_shared/popover/index.ts create mode 100644 frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx create mode 100644 frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx create mode 100644 frontend/appflowy_web_app/src/components/_shared/tag/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/Database.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/board/Board.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/board/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/calendar/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/cell.parse.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/conditions/context.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/conditions/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/field/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/package.json create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-cell/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/sorts/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/tabs/ViewTabs.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/tabs/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/grid/Grid.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/grid/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/index.ts create mode 100644 frontend/appflowy_web_app/src/pages/DatabasePage.tsx diff --git a/frontend/appflowy_tauri/style-dictionary/tokens/base.json b/frontend/appflowy_tauri/style-dictionary/tokens/base.json index 4e31b0523d..fb58a867b1 100644 --- a/frontend/appflowy_tauri/style-dictionary/tokens/base.json +++ b/frontend/appflowy_tauri/style-dictionary/tokens/base.json @@ -7,7 +7,7 @@ "type": "color" }, "100": { - "value": "#edeef2", + "value": "#dadbdd", "type": "color" }, "200": { diff --git a/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json b/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json new file mode 100644 index 0000000000..6961f6f1c4 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json @@ -0,0 +1,61 @@ +{ + "data": { + "user_profile": { + "uid": 304120109071339520, + "uuid": "cbff060a-196d-415a-aa80-759c01886466", + "email": "lu@appflowy.io", + "password": "", + "name": "Kilu", + "metadata": { + "icon_url": "🇽🇰" + }, + "encryption_sign": null, + "latest_workspace_id": "9eebea03-3ed5-4298-86b2-a7f77856d48b", + "updated_at": 1715847453 + }, + "visiting_workspace": { + "workspace_id": "9eebea03-3ed5-4298-86b2-a7f77856d48b", + "database_storage_id": "375874be-7a4f-4b7c-8b89-1dc9a39838f4", + "owner_uid": 304120109071339520, + "owner_name": "Kilu", + "workspace_type": 0, + "workspace_name": "Kilu Works", + "created_at": "2024-03-13T07:23:10.275174Z", + "icon": "😆" + }, + "workspaces": [ + { + "workspace_id": "81570fa8-8be9-4b2d-9f1c-1ef4f34079a8", + "database_storage_id": "6c1f1a2c-e8d5-4bc2-917f-495bce862abb", + "owner_uid": 311828434584080384, + "owner_name": "Zack Zi Xiang Fu", + "workspace_type": 0, + "workspace_name": "My Workspace", + "created_at": "2024-04-03T13:53:18.295918Z", + "icon": "" + }, + { + "workspace_id": "fcb503f9-9287-4de4-8de0-ea191e680968", + "database_storage_id": "ae1b82a5-2b93-45c7-901a-f9357c544534", + "owner_uid": 276169796100296704, + "owner_name": "Annie Anqi Wang", + "workspace_type": 0, + "workspace_name": "AppFlowy Test", + "created_at": "2023-12-27T04:18:36.372013Z", + "icon": "" + }, + { + "workspace_id": "9eebea03-3ed5-4298-86b2-a7f77856d48b", + "database_storage_id": "375874be-7a4f-4b7c-8b89-1dc9a39838f4", + "owner_uid": 304120109071339520, + "owner_name": "Kilu", + "workspace_type": 0, + "workspace_name": "Kilu Works", + "created_at": "2024-03-13T07:23:10.275174Z", + "icon": "😆" + } + ] + }, + "code": 0, + "message": "Operation completed successfully." +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/support/commands.ts b/frontend/appflowy_web_app/cypress/support/commands.ts index 6146bd1c01..b275a842c5 100644 --- a/frontend/appflowy_web_app/cypress/support/commands.ts +++ b/frontend/appflowy_web_app/cypress/support/commands.ts @@ -37,6 +37,7 @@ Cypress.Commands.add('mockAPI', () => { cy.intercept('POST', '/gotrue/token?grant_type=refresh_token', json).as('refreshToken'); }); cy.intercept('GET', '/api/user/profile', { fixture: 'user' }).as('getUserProfile'); + cy.intercept('GET', '/api/user/workspace', { fixture: 'user_workspace' }).as('getUserWorkspace'); }); // Example use: diff --git a/frontend/appflowy_web_app/index.html b/frontend/appflowy_web_app/index.html index 3548e9b85d..5480f37859 100644 --- a/frontend/appflowy_web_app/index.html +++ b/frontend/appflowy_web_app/index.html @@ -3,7 +3,9 @@ - + AppFlowy diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index 1acc7d6e82..2dafe5e66d 100644 --- a/frontend/appflowy_web_app/package.json +++ b/frontend/appflowy_web_app/package.json @@ -22,12 +22,13 @@ "test:unit": "jest" }, "dependencies": { - "@appflowyinc/client-api-wasm": "0.0.2-alpha.2", + "@appflowyinc/client-api-wasm": "0.0.3", "@atlaskit/primitives": "^5.5.3", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", + "@jest/globals": "^29.7.0", "@mui/icons-material": "^5.11.11", "@mui/material": "6.0.0-alpha.2", "@mui/x-date-pickers-pro": "^6.18.2", @@ -35,9 +36,10 @@ "@slate-yjs/core": "^1.0.2", "@tauri-apps/api": "^1.5.3", "@types/react-swipeable-views": "^0.13.4", + "async-retry": "^1.3.3", "axios": "^1.6.8", "dayjs": "^1.11.9", - "dexie": "^4.0.1", + "decimal.js": "^10.4.3", "emoji-mart": "^5.5.2", "emoji-regex": "^10.2.1", "events": "^3.3.0", @@ -51,6 +53,7 @@ "katex": "^0.16.7", "lodash-es": "^4.17.21", "nanoid": "^4.0.0", + "numeral": "^2.0.6", "prismjs": "^1.29.0", "protoc-gen-ts": "0.8.7", "quill": "^1.3.7", @@ -66,6 +69,7 @@ "react-hot-toast": "^2.4.1", "react-i18next": "^14.1.0", "react-katex": "^3.0.1", + "react-measure": "^2.5.2", "react-redux": "^8.0.5", "react-router-dom": "^6.22.3", "react-swipeable-views": "^0.14.0", @@ -98,6 +102,7 @@ "@types/katex": "^0.16.0", "@types/lodash-es": "^4.17.11", "@types/node": "^20.11.30", + "@types/numeral": "^2.0.5", "@types/prismjs": "^1.26.0", "@types/quill": "^2.0.10", "@types/react": "^18.2.66", @@ -107,6 +112,7 @@ "@types/react-datepicker": "^4.19.3", "@types/react-dom": "^18.2.22", "@types/react-katex": "^3.0.0", + "@types/react-measure": "^2.0.12", "@types/react-transition-group": "^4.4.6", "@types/react-window": "^1.8.8", "@types/utf8": "^3.0.1", diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index b9fe83de2f..770298d3b9 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@appflowyinc/client-api-wasm': - specifier: 0.0.2-alpha.2 - version: 0.0.2-alpha.2 + specifier: 0.0.3 + version: 0.0.3 '@atlaskit/primitives': specifier: ^5.5.3 version: 5.5.3(@types/react@18.2.66)(react@18.2.0) @@ -23,6 +23,9 @@ dependencies: '@emotion/styled': specifier: ^11.10.6 version: 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 '@mui/icons-material': specifier: ^5.11.11 version: 5.11.11(@mui/material@6.0.0-alpha.2)(@types/react@18.2.66)(react@18.2.0) @@ -44,15 +47,18 @@ dependencies: '@types/react-swipeable-views': specifier: ^0.13.4 version: 0.13.4 + async-retry: + specifier: ^1.3.3 + version: 1.3.3 axios: specifier: ^1.6.8 version: 1.6.8 dayjs: specifier: ^1.11.9 version: 1.11.9 - dexie: - specifier: ^4.0.1 - version: 4.0.1 + decimal.js: + specifier: ^10.4.3 + version: 10.4.3 emoji-mart: specifier: ^5.5.2 version: 5.5.2 @@ -92,6 +98,9 @@ dependencies: nanoid: specifier: ^4.0.0 version: 4.0.0 + numeral: + specifier: ^2.0.6 + version: 2.0.6 prismjs: specifier: ^1.29.0 version: 1.29.0 @@ -137,6 +146,9 @@ dependencies: react-katex: specifier: ^3.0.1 version: 3.0.1(prop-types@15.8.1)(react@18.2.0) + react-measure: + specifier: ^2.5.2 + version: 2.5.2(react-dom@18.2.0)(react@18.2.0) react-redux: specifier: ^8.0.5 version: 8.0.5(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) @@ -229,6 +241,9 @@ devDependencies: '@types/node': specifier: ^20.11.30 version: 20.11.30 + '@types/numeral': + specifier: ^2.0.5 + version: 2.0.5 '@types/prismjs': specifier: ^1.26.0 version: 1.26.0 @@ -256,6 +271,9 @@ devDependencies: '@types/react-katex': specifier: ^3.0.0 version: 3.0.0 + '@types/react-measure': + specifier: ^2.0.12 + version: 2.0.12 '@types/react-transition-group': specifier: ^4.4.6 version: 4.4.6 @@ -376,8 +394,8 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - /@appflowyinc/client-api-wasm@0.0.2-alpha.2: - resolution: {integrity: sha512-BcRK06zHHJdaGNYohYxGaR2xPfQ1RwU48jMzdMZDf2HXVLU2WWQ6cYfuM4lrsK+O3QEfJdeEL2fntnQDaaeQng==} + /@appflowyinc/client-api-wasm@0.0.3: + resolution: {integrity: sha512-ARjLhiDZ8MiZ9egWDbAX9VAdXXS30av+InCPLrS/iqCMYrhuuU9rxS9jQeNEB7jucFrj158gBRusimFN7P/lyw==} dev: false /@atlaskit/analytics-next-stable-react-context@1.0.1(react@18.2.0): @@ -2677,6 +2695,10 @@ packages: dependencies: undici-types: 5.26.5 + /@types/numeral@2.0.5: + resolution: {integrity: sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw==} + dev: true + /@types/parse-json@4.0.2: resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} dev: false @@ -2737,6 +2759,12 @@ packages: '@types/react': 18.2.66 dev: true + /@types/react-measure@2.0.12: + resolution: {integrity: sha512-Y6V11CH6bU7RhqrIdENPwEUZlPXhfXNGylMNnGwq5TAEs2wDoBA3kSVVM/EQ8u72sz5r9ja+7W8M8PIVcS841Q==} + dependencies: + '@types/react': 18.2.66 + dev: true + /@types/react-redux@7.1.33: resolution: {integrity: sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==} dependencies: @@ -3234,6 +3262,12 @@ packages: engines: {node: '>=8'} dev: true + /async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + dependencies: + retry: 0.13.1 + dev: false + /async@3.2.5: resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} dev: true @@ -4015,7 +4049,6 @@ packages: /decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} - dev: true /dedent@1.5.1: resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} @@ -4101,10 +4134,6 @@ packages: minimist: 1.2.8 dev: true - /dexie@4.0.1: - resolution: {integrity: sha512-wSNn+TcCh+DuE2pdg058K3MhxA4g+IiZlW7yGz4cMd/t3z2rJXZcV3HDxZljbrICU2Iq0qY4UHnbolTMK/+bcA==} - dev: false - /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true @@ -4875,6 +4904,10 @@ packages: has-symbols: 1.0.3 hasown: 2.0.2 + /get-node-dimensions@1.2.1: + resolution: {integrity: sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==} + dev: false + /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -6384,6 +6417,10 @@ packages: boolbase: 1.0.0 dev: true + /numeral@2.0.6: + resolution: {integrity: sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==} + dev: false + /nwsapi@2.2.7: resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} dev: true @@ -7138,6 +7175,20 @@ packages: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} dev: false + /react-measure@2.5.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA==} + peerDependencies: + react: '>0.13.0' + react-dom: '>0.13.0' + dependencies: + '@babel/runtime': 7.24.4 + get-node-dimensions: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + resize-observer-polyfill: 1.5.1 + 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: @@ -7452,6 +7503,10 @@ packages: resolution: {integrity: sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==} dev: false + /resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + dev: false + /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -7495,6 +7550,11 @@ packages: signal-exit: 3.0.7 dev: true + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: false + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} diff --git a/frontend/appflowy_web_app/src/application/collab.type.ts b/frontend/appflowy_web_app/src/application/collab.type.ts index 0df2729749..9a2bcfe186 100644 --- a/frontend/appflowy_web_app/src/application/collab.type.ts +++ b/frontend/appflowy_web_app/src/application/collab.type.ts @@ -1,4 +1,4 @@ -import Y from 'yjs'; +import * as Y from 'yjs'; export type BlockId = string; @@ -8,6 +8,10 @@ export type ChildrenId = string; export type ViewId = string; +export type RowId = string; + +export type CellId = string; + export enum BlockType { Paragraph = 'paragraph', Page = 'page', @@ -192,6 +196,51 @@ export enum YjsFolderKey { type = 'ty', value = 'value', layout = 'layout', + bid = 'bid', +} + +export enum YjsDatabaseKey { + views = 'views', + id = 'id', + metas = 'metas', + fields = 'fields', + is_primary = 'is_primary', + last_modified = 'last_modified', + created_at = 'created_at', + name = 'name', + type = 'ty', + type_option = 'type_option', + content = 'content', + data = 'data', + iid = 'iid', + database_id = 'database_id', + field_orders = 'field_orders', + field_settings = 'field_settings', + visibility = 'visibility', + wrap = 'wrap', + width = 'width', + filters = 'filters', + groups = 'groups', + layout = 'layout', + layout_settings = 'layout_settings', + modified_at = 'modified_at', + row_orders = 'row_orders', + sorts = 'sorts', + height = 'height', + cells = 'cells', + field_type = 'field_type', + end_timestamp = 'end_timestamp', + include_time = 'include_time', + is_range = 'is_range', + reminder_id = 'reminder_id', + time_format = 'time_format', + date_format = 'date_format', + calculations = 'calculations', + field_id = 'field_id', + calculation_value = 'calculation_value', + condition = 'condition', + format = 'format', + filter_type = 'filter_type', } export interface YDoc extends Y.Doc { @@ -199,11 +248,54 @@ export interface YDoc extends Y.Doc { getMap(key: YjsEditorKey.data_section): YSharedRoot | any; } +export interface YDatabaseRow extends Y.Map { + get(key: YjsDatabaseKey.id): RowId; + + get(key: YjsDatabaseKey.height): string; + + get(key: YjsDatabaseKey.visibility): boolean; + + get(key: YjsDatabaseKey.created_at): CreatedAt; + + get(key: YjsDatabaseKey.last_modified): LastModified; + + get(key: YjsDatabaseKey.cells): YDatabaseCells; +} + +export interface YDatabaseCells extends Y.Map { + get(key: FieldId): YDatabaseCell; +} + +export type EndTimestamp = string; +export type ReminderId = string; + +export interface YDatabaseCell extends Y.Map { + get(key: YjsDatabaseKey.created_at): CreatedAt; + + get(key: YjsDatabaseKey.last_modified): LastModified; + + get(key: YjsDatabaseKey.field_type): string; + + get(key: YjsDatabaseKey.data): object | string | boolean | number; + + get(key: YjsDatabaseKey.end_timestamp): EndTimestamp; + + get(key: YjsDatabaseKey.include_time): boolean; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.is_range): boolean; + + get(key: YjsDatabaseKey.reminder_id): ReminderId; +} + export interface YSharedRoot extends Y.Map { get(key: YjsEditorKey.document): YDocument; - // eslint-disable-next-line @typescript-eslint/unified-signatures get(key: YjsEditorKey.folder): YFolder; + + get(key: YjsEditorKey.database): YDatabase; + + get(key: YjsEditorKey.database_row): YDatabaseRow; } export interface YFolder extends Y.Map { @@ -226,6 +318,9 @@ export interface YViews extends Y.Map { export interface YView extends Y.Map { get(key: YjsFolderKey.id): ViewId; + get(key: YjsFolderKey.bid): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures get(key: YjsFolderKey.name): string; // eslint-disable-next-line @typescript-eslint/unified-signatures @@ -271,6 +366,166 @@ export interface YTextMap extends Y.Map { get(key: ExternalId): Y.Text; } +export interface YDatabase extends Y.Map { + get(key: YjsDatabaseKey.views): YDatabaseViews; + + get(key: YjsDatabaseKey.metas): YDatabaseMetas; + + get(key: YjsDatabaseKey.fields): YDatabaseFields; + + get(key: YjsDatabaseKey.id): string; +} + +export interface YDatabaseViews extends Y.Map { + get(key: ViewId): YDatabaseView; +} + +export type DatabaseId = string; +export type CreatedAt = string; +export type LastModified = string; +export type ModifiedAt = string; +export type FieldId = string; + +export enum DatabaseViewLayout { + Grid = 0, + Board = 1, + Calendar = 2, +} + +export interface YDatabaseView extends Y.Map { + get(key: YjsDatabaseKey.database_id): DatabaseId; + + get(key: YjsDatabaseKey.name): string; + + get(key: YjsDatabaseKey.created_at): CreatedAt; + + get(key: YjsDatabaseKey.modified_at): ModifiedAt; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.layout): string; + + get(key: YjsDatabaseKey.layout_settings): YDatabaseLayoutSettings; + + get(key: YjsDatabaseKey.filters): YDatabaseFilters; + + get(key: YjsDatabaseKey.groups): YDatabaseGroups; + + get(key: YjsDatabaseKey.sorts): YDatabaseSorts; + + get(key: YjsDatabaseKey.field_settings): YDatabaseFieldSettings; + + get(key: YjsDatabaseKey.field_orders): YDatabaseFieldOrders; + + get(key: YjsDatabaseKey.row_orders): YDatabaseRowOrders; + + get(key: YjsDatabaseKey.calculations): YDatabaseCalculations; +} + +export type YDatabaseFieldOrders = Y.Array; // [ { id: FieldId } ] + +export type YDatabaseRowOrders = Y.Array; // [ { id: RowId, height: number } ] + +export type YDatabaseGroups = Y.Array; + +export type YDatabaseFilters = Y.Array; + +export type YDatabaseSorts = Y.Array; + +export type YDatabaseLayoutSettings = Y.Map; + +export type YDatabaseCalculations = Y.Array; + +export type SortId = string; + +export interface YDatabaseRowOrder extends Y.Map { + get(key: YjsDatabaseKey.id): SortId; + + get(key: YjsDatabaseKey.height): number; +} + +export interface YDatabaseSort extends Y.Map { + get(key: YjsDatabaseKey.id): SortId; + + get(key: YjsDatabaseKey.field_id): FieldId; + + get(key: YjsDatabaseKey.condition): string; +} + +export type FilterId = string; + +export interface YDatabaseFilter extends Y.Map { + get(key: YjsDatabaseKey.id): FilterId; + + get(key: YjsDatabaseKey.field_id): FieldId; + + get(key: YjsDatabaseKey.type | YjsDatabaseKey.condition | YjsDatabaseKey.content | YjsDatabaseKey.filter_type): string; +} + +export interface YDatabaseCalculation extends Y.Map { + get(key: YjsDatabaseKey.field_id): FieldId; + + get(key: YjsDatabaseKey.id | YjsDatabaseKey.type | YjsDatabaseKey.calculation_value): string; +} + +export interface YDatabaseFieldSettings extends Y.Map { + get(key: FieldId): YDatabaseFieldSetting; +} + +export interface YDatabaseFieldSetting extends Y.Map { + get(key: YjsDatabaseKey.visibility): string; + + get(key: YjsDatabaseKey.wrap): boolean; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.width): string; +} + +export interface YDatabaseMetas extends Y.Map { + get(key: YjsDatabaseKey.iid): string; +} + +export interface YDatabaseFields extends Y.Map { + get(key: FieldId): YDatabaseField; +} + +export interface YDatabaseField extends Y.Map { + get(key: YjsDatabaseKey.name): string; + + get(key: YjsDatabaseKey.id): FieldId; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.type): string; + + get(key: YjsDatabaseKey.type_option): YDatabaseFieldTypeOption; + + get(key: YjsDatabaseKey.is_primary): boolean; + + get(key: YjsDatabaseKey.last_modified): LastModified; +} + +export interface YDatabaseFieldTypeOption extends Y.Map { + // key is the field type + get(key: string): YMapFieldTypeOption; +} + +export interface YMapFieldTypeOption extends Y.Map { + get(key: YjsDatabaseKey.content): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.data): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.time_format): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.date_format): string; + + get(key: YjsDatabaseKey.database_id): DatabaseId; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.format): string; +} + export enum CollabType { Document = 0, Database = 1, @@ -282,8 +537,12 @@ export enum CollabType { } export enum CollabOrigin { + // from local changes and never sync to remote. used for read-only mode Local = 'local', + // from remote changes and never sync to remote. Remote = 'remote', + // from local changes and sync to remote. used for collaborative mode + LocalSync = 'local_sync', } export const layoutMap = { @@ -292,3 +551,9 @@ export const layoutMap = { [ViewLayout.Board]: 'board', [ViewLayout.Calendar]: 'calendar', }; + +export const databaseLayoutMap = { + [DatabaseViewLayout.Grid]: 'grid', + [DatabaseViewLayout.Board]: 'board', + [DatabaseViewLayout.Calendar]: 'calendar', +}; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/const.ts b/frontend/appflowy_web_app/src/application/database-yjs/const.ts new file mode 100644 index 0000000000..b082acc6a4 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/const.ts @@ -0,0 +1,2 @@ +export const DEFAULT_ROW_HEIGHT = 37; +export const MIN_COLUMN_WIDTH = 100; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/context.ts b/frontend/appflowy_web_app/src/application/database-yjs/context.ts new file mode 100644 index 0000000000..8717aa0ffe --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/context.ts @@ -0,0 +1,127 @@ +import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { filterBy } from '@/application/database-yjs/filter'; +import { Row } from '@/application/database-yjs/selector'; +import { sortBy } from '@/application/database-yjs/sort'; +import { createContext, useContext, useEffect, useState } from 'react'; +import * as Y from 'yjs'; +import debounce from 'lodash-es/debounce'; + +export interface DatabaseContextState { + readOnly: boolean; + doc: YDoc; + viewId: string; + rowDocMap: Y.Map; +} + +export const DatabaseContext = createContext(null); + +export const useDatabase = () => { + const database = useContext(DatabaseContext) + ?.doc?.getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.database) as YDatabase; + + return database; +}; + +export const useRowMeta = (rowId: string) => { + const rows = useContext(DatabaseContext)?.rowDocMap; + const rowMetaDoc = rows?.get(rowId); + const rowMeta = rowMetaDoc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + return rowMeta; +}; + +export const useViewId = () => { + const context = useContext(DatabaseContext); + + return context?.viewId; +}; + +export const useReadOnly = () => { + const context = useContext(DatabaseContext); + + return context?.readOnly; +}; + +export const useDatabaseView = () => { + const database = useDatabase(); + const viewId = useViewId(); + + return viewId ? database.get(YjsDatabaseKey.views)?.get(viewId) : undefined; +}; + +export function useDatabaseFields() { + const database = useDatabase(); + + return database.get(YjsDatabaseKey.fields); +} + +export interface GridRowsState { + rowOrders: Row[]; +} + +export const GridRowsContext = createContext(null); + +export function useGridRowsContext() { + return useContext(GridRowsContext); +} + +export function useGridRows() { + return useGridRowsContext()?.rowOrders; +} + +export function useGridRowOrders() { + const rows = useContext(DatabaseContext)?.rowDocMap; + const [rowOrders, setRowOrders] = useState(); + const view = useDatabaseView(); + const sorts = view?.get(YjsDatabaseKey.sorts); + const fields = useDatabaseFields(); + const filters = view?.get(YjsDatabaseKey.filters); + + useEffect(() => { + const onConditionsChange = () => { + const originalRowOrders = view?.get(YjsDatabaseKey.row_orders).toJSON(); + + if (!originalRowOrders || !rows) return; + + console.log('sort or filter changed'); + if (sorts?.length === 0 && filters?.length === 0) { + setRowOrders(originalRowOrders); + return; + } + + let rowOrders: Row[] | undefined; + + if (sorts?.length) { + rowOrders = sortBy(originalRowOrders, sorts, fields, rows); + } + + if (filters?.length) { + rowOrders = filterBy(rowOrders ?? originalRowOrders, filters, fields, rows); + } + + if (rowOrders) { + setRowOrders(rowOrders); + } else { + setRowOrders(originalRowOrders); + } + }; + + const debounceConditionsChange = debounce(onConditionsChange, 200); + + onConditionsChange(); + sorts?.observeDeep(debounceConditionsChange); + filters?.observeDeep(debounceConditionsChange); + fields?.observeDeep(debounceConditionsChange); + rows?.observeDeep(debounceConditionsChange); + + return () => { + sorts?.unobserveDeep(debounceConditionsChange); + filters?.unobserveDeep(debounceConditionsChange); + fields?.unobserveDeep(debounceConditionsChange); + rows?.observeDeep(debounceConditionsChange); + }; + }, [fields, rows, sorts, filters, view]); + + return rowOrders; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts new file mode 100644 index 0000000000..f5d4aeac61 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts @@ -0,0 +1,51 @@ +import { FieldId } from '@/application/collab.type'; + +export enum FieldVisibility { + AlwaysShown = 0, + HideWhenEmpty = 1, + AlwaysHidden = 2, +} + +export enum FieldType { + RichText = 0, + Number = 1, + DateTime = 2, + SingleSelect = 3, + MultiSelect = 4, + Checkbox = 5, + URL = 6, + Checklist = 7, + LastEditedTime = 8, + CreatedTime = 9, + Relation = 10, +} + +export enum CalculationType { + Average = 0, + Max = 1, + Median = 2, + Min = 3, + Sum = 4, + Count = 5, + CountEmpty = 6, + CountNonEmpty = 7, +} + +export enum SortCondition { + Ascending = 0, + Descending = 1, +} + +export enum FilterType { + Data = 0, + And = 1, + Or = 2, +} + +export interface Filter { + fieldId: FieldId; + filterType: FilterType; + condition: number; + id: string; + content: string; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts new file mode 100644 index 0000000000..b9da4341f6 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts @@ -0,0 +1,10 @@ +import { Filter } from '@/application/database-yjs'; + +export enum CheckboxFilterCondition { + IsChecked = 0, + IsUnChecked = 1, +} + +export interface CheckboxFilter extends Filter { + condition: CheckboxFilterCondition; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts new file mode 100644 index 0000000000..9ccd409dc8 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts @@ -0,0 +1 @@ +export * from './checkbox.type'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts new file mode 100644 index 0000000000..2b504ded8a --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts @@ -0,0 +1,10 @@ +import { Filter } from '@/application/database-yjs'; + +export enum ChecklistFilterCondition { + IsComplete = 0, + IsIncomplete = 1, +} + +export interface ChecklistFilter extends Filter { + condition: ChecklistFilterCondition; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts new file mode 100644 index 0000000000..15d37f912b --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts @@ -0,0 +1,2 @@ +export * from './checklist.type'; +export * from './parse'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts new file mode 100644 index 0000000000..6dd14c71e0 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts @@ -0,0 +1,22 @@ +import { SelectOption } from '../select-option'; + +export interface ChecklistCellData { + selectedOptionIds?: string[]; + options?: SelectOption[]; + percentage: number; +} + +export function parseChecklistData(data: string): ChecklistCellData | null { + try { + const { options, selected_option_ids } = JSON.parse(data); + const percentage = (selected_option_ids.length / options.length) * 100; + + return { + percentage, + options, + selectedOptionIds: selected_option_ids, + }; + } catch (e) { + return null; + } +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts new file mode 100644 index 0000000000..0db15f21eb --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts @@ -0,0 +1,32 @@ +import { Filter } from '@/application/database-yjs'; + +export enum TimeFormat { + TwelveHour = 0, + TwentyFourHour = 1, +} + +export enum DateFormat { + Local = 0, + US = 1, + ISO = 2, + Friendly = 3, + DayMonthYear = 4, +} + +export enum DateFilterCondition { + DateIs = 0, + DateBefore = 1, + DateAfter = 2, + DateOnOrBefore = 3, + DateOnOrAfter = 4, + DateWithIn = 5, + DateIsEmpty = 6, + DateIsNotEmpty = 7, +} + +export interface DateFilter extends Filter { + condition: DateFilterCondition; + start?: number; + end?: number; + timestamp?: number; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts new file mode 100644 index 0000000000..106279c949 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts @@ -0,0 +1,2 @@ +export * from './date.type'; +export * from './utils'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts new file mode 100644 index 0000000000..985402768b --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts @@ -0,0 +1,29 @@ +import { TimeFormat, DateFormat } from '@/application/database-yjs'; + +export function getTimeFormat(timeFormat?: TimeFormat) { + switch (timeFormat) { + case TimeFormat.TwelveHour: + return 'h:mm A'; + case TimeFormat.TwentyFourHour: + return 'HH:mm'; + default: + return 'HH:mm'; + } +} + +export function getDateFormat(dateFormat?: DateFormat) { + switch (dateFormat) { + case DateFormat.Friendly: + return 'MMM DD, YYYY'; + case DateFormat.ISO: + return 'YYYY-MM-DD'; + case DateFormat.US: + return 'YYYY/MM/DD'; + case DateFormat.Local: + return 'MM/DD/YYYY'; + case DateFormat.DayMonthYear: + return 'DD/MM/YYYY'; + default: + return 'YYYY-MM-DD'; + } +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts new file mode 100644 index 0000000000..5505f0e4ed --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts @@ -0,0 +1,8 @@ +export * from './type_option'; +export * from './date'; +export * from './number'; +export * from './select-option'; +export * from './text'; +export * from './checkbox'; +export * from './checklist'; +export * from './relation'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts new file mode 100644 index 0000000000..e165752348 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts @@ -0,0 +1,628 @@ +import { currencyFormaterMap } from '../format'; +import { NumberFormat } from '../number.type'; +import { expect } from '@jest/globals'; + +const testCases = [0, 1, 0.5, 0.5666, 1000, 10000, 1000000, 10000000, 1000000.0]; +describe('currencyFormaterMap', () => { + test('should return the correct formatter for Num', () => { + const formater = currencyFormaterMap[NumberFormat.Num]; + const result = ['0', '1', '0.5', '0.5666', '1,000', '10,000', '1,000,000', '10,000,000', '1,000,000']; + testCases.forEach((testCase) => { + expect(formater(testCase)).toBe(result[testCases.indexOf(testCase)]); + }); + }); + + test('should return the correct formatter for Percent', () => { + const formater = currencyFormaterMap[NumberFormat.Percent]; + const result = ['0%', '1%', '0.5%', '0.57%', '1,000%', '10,000%', '1,000,000%', '10,000,000%', '1,000,000%']; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for USD', () => { + const formater = currencyFormaterMap[NumberFormat.USD]; + const result = ['$0', '$1', '$0.5', '$0.57', '$1,000', '$10,000', '$1,000,000', '$10,000,000', '$1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for CanadianDollar', () => { + const formater = currencyFormaterMap[NumberFormat.CanadianDollar]; + const result = [ + 'CA$0', + 'CA$1', + 'CA$0.5', + 'CA$0.57', + 'CA$1,000', + 'CA$10,000', + 'CA$1,000,000', + 'CA$10,000,000', + 'CA$1,000,000', + ]; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for EUR', () => { + const formater = currencyFormaterMap[NumberFormat.EUR]; + + const result = ['€0', '€1', '€0.5', '€0.57', '€1,000', '€10,000', '€1,000,000', '€10,000,000', '€1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Pound', () => { + const formater = currencyFormaterMap[NumberFormat.Pound]; + + const result = ['£0', '£1', '£0.5', '£0.57', '£1,000', '£10,000', '£1,000,000', '£10,000,000', '£1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Yen', () => { + const formater = currencyFormaterMap[NumberFormat.Yen]; + + const result = [ + '¥0', + '¥1', + '¥0.5', + '¥0.57', + '¥1,000', + '¥10,000', + '¥1,000,000', + '¥10,000,000', + '¥1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Ruble', () => { + const formater = currencyFormaterMap[NumberFormat.Ruble]; + + const result = [ + '0 RUB', + '1 RUB', + '0,5 RUB', + '0,57 RUB', + '1 000 RUB', + '10 000 RUB', + '1 000 000 RUB', + '10 000 000 RUB', + '1 000 000 RUB', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Rupee', () => { + const formater = currencyFormaterMap[NumberFormat.Rupee]; + + const result = ['₹0', '₹1', '₹0.5', '₹0.57', '₹1,000', '₹10,000', '₹10,00,000', '₹1,00,00,000', '₹10,00,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Won', () => { + const formater = currencyFormaterMap[NumberFormat.Won]; + + const result = ['₩0', '₩1', '₩0.5', '₩0.57', '₩1,000', '₩10,000', '₩1,000,000', '₩10,000,000', '₩1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Yuan', () => { + const formater = currencyFormaterMap[NumberFormat.Yuan]; + + const result = [ + 'CN¥0', + 'CN¥1', + 'CN¥0.5', + 'CN¥0.57', + 'CN¥1,000', + 'CN¥10,000', + 'CN¥1,000,000', + 'CN¥10,000,000', + 'CN¥1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Real', () => { + const formater = currencyFormaterMap[NumberFormat.Real]; + + const result = [ + 'R$ 0', + 'R$ 1', + 'R$ 0,5', + 'R$ 0,57', + 'R$ 1.000', + 'R$ 10.000', + 'R$ 1.000.000', + 'R$ 10.000.000', + 'R$ 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Lira', () => { + const formater = currencyFormaterMap[NumberFormat.Lira]; + + const result = [ + 'TRY 0', + 'TRY 1', + 'TRY 0,5', + 'TRY 0,57', + 'TRY 1.000', + 'TRY 10.000', + 'TRY 1.000.000', + 'TRY 10.000.000', + 'TRY 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Rupiah', () => { + const formater = currencyFormaterMap[NumberFormat.Rupiah]; + + const result = [ + 'IDR 0', + 'IDR 1', + 'IDR 0,5', + 'IDR 0,57', + 'IDR 1.000', + 'IDR 10.000', + 'IDR 1.000.000', + 'IDR 10.000.000', + 'IDR 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Franc', () => { + const formater = currencyFormaterMap[NumberFormat.Franc]; + + const result = [ + 'CHF 0', + 'CHF 1', + 'CHF 0.5', + 'CHF 0.57', + `CHF 1’000`, + `CHF 10’000`, + `CHF 1’000’000`, + `CHF 10’000’000`, + `CHF 1’000’000`, + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for HongKongDollar', () => { + const formater = currencyFormaterMap[NumberFormat.HongKongDollar]; + + const result = [ + 'HK$0', + 'HK$1', + 'HK$0.5', + 'HK$0.57', + 'HK$1,000', + 'HK$10,000', + 'HK$1,000,000', + 'HK$10,000,000', + 'HK$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for NewZealandDollar', () => { + const formater = currencyFormaterMap[NumberFormat.NewZealandDollar]; + + const result = [ + 'NZ$0', + 'NZ$1', + 'NZ$0.5', + 'NZ$0.57', + 'NZ$1,000', + 'NZ$10,000', + 'NZ$1,000,000', + 'NZ$10,000,000', + 'NZ$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Krona', () => { + const formater = currencyFormaterMap[NumberFormat.Krona]; + + const result = [ + '0 SEK', + '1 SEK', + '0,5 SEK', + '0,57 SEK', + '1 000 SEK', + '10 000 SEK', + '1 000 000 SEK', + '10 000 000 SEK', + '1 000 000 SEK', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for NorwegianKrone', () => { + const formater = currencyFormaterMap[NumberFormat.NorwegianKrone]; + + const result = [ + 'NOK 0', + 'NOK 1', + 'NOK 0,5', + 'NOK 0,57', + 'NOK 1 000', + 'NOK 10 000', + 'NOK 1 000 000', + 'NOK 10 000 000', + 'NOK 1 000 000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for MexicanPeso', () => { + const formater = currencyFormaterMap[NumberFormat.MexicanPeso]; + + const result = [ + 'MX$0', + 'MX$1', + 'MX$0.5', + 'MX$0.57', + 'MX$1,000', + 'MX$10,000', + 'MX$1,000,000', + 'MX$10,000,000', + 'MX$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Rand', () => { + const formater = currencyFormaterMap[NumberFormat.Rand]; + + const result = [ + 'ZAR 0', + 'ZAR 1', + 'ZAR 0,5', + 'ZAR 0,57', + 'ZAR 1 000', + 'ZAR 10 000', + 'ZAR 1 000 000', + 'ZAR 10 000 000', + 'ZAR 1 000 000', + ]; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for NewTaiwanDollar', () => { + const formater = currencyFormaterMap[NumberFormat.NewTaiwanDollar]; + + const result = [ + 'NT$0', + 'NT$1', + 'NT$0.5', + 'NT$0.57', + 'NT$1,000', + 'NT$10,000', + 'NT$1,000,000', + 'NT$10,000,000', + 'NT$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for DanishKrone', () => { + const formater = currencyFormaterMap[NumberFormat.DanishKrone]; + + const result = [ + '0 DKK', + '1 DKK', + '0,5 DKK', + '0,57 DKK', + '1.000 DKK', + '10.000 DKK', + '1.000.000 DKK', + '10.000.000 DKK', + '1.000.000 DKK', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Baht', () => { + const formater = currencyFormaterMap[NumberFormat.Baht]; + + const result = [ + 'THB 0', + 'THB 1', + 'THB 0.5', + 'THB 0.57', + 'THB 1,000', + 'THB 10,000', + 'THB 1,000,000', + 'THB 10,000,000', + 'THB 1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Forint', () => { + const formater = currencyFormaterMap[NumberFormat.Forint]; + + const result = [ + '0 HUF', + '1 HUF', + '0,5 HUF', + '0,57 HUF', + '1 000 HUF', + '10 000 HUF', + '1 000 000 HUF', + '10 000 000 HUF', + '1 000 000 HUF', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Koruna', () => { + const formater = currencyFormaterMap[NumberFormat.Koruna]; + + const result = [ + '0 CZK', + '1 CZK', + '0,5 CZK', + '0,57 CZK', + '1 000 CZK', + '10 000 CZK', + '1 000 000 CZK', + '10 000 000 CZK', + '1 000 000 CZK', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Shekel', () => { + const formater = currencyFormaterMap[NumberFormat.Shekel]; + + const result = [ + '‏0 ‏₪', + '‏1 ‏₪', + '‏0.5 ‏₪', + '‏0.57 ‏₪', + '‏1,000 ‏₪', + '‏10,000 ‏₪', + '‏1,000,000 ‏₪', + '‏10,000,000 ‏₪', + '‏1,000,000 ‏₪', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for ChileanPeso', () => { + const formater = currencyFormaterMap[NumberFormat.ChileanPeso]; + + const result = [ + 'CLP 0', + 'CLP 1', + 'CLP 0,5', + 'CLP 0,57', + 'CLP 1.000', + 'CLP 10.000', + 'CLP 1.000.000', + 'CLP 10.000.000', + 'CLP 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for PhilippinePeso', () => { + const formater = currencyFormaterMap[NumberFormat.PhilippinePeso]; + + const result = ['₱0', '₱1', '₱0.5', '₱0.57', '₱1,000', '₱10,000', '₱1,000,000', '₱10,000,000', '₱1,000,000']; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Dirham', () => { + const formater = currencyFormaterMap[NumberFormat.Dirham]; + + const result = [ + '‏0 AED', + '‏1 AED', + '‏0.5 AED', + '‏0.57 AED', + '‏1,000 AED', + '‏10,000 AED', + '‏1,000,000 AED', + '‏10,000,000 AED', + '‏1,000,000 AED', + ]; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for ColombianPeso', () => { + const formater = currencyFormaterMap[NumberFormat.ColombianPeso]; + + const result = [ + 'COP 0', + 'COP 1', + 'COP 0,5', + 'COP 0,57', + 'COP 1.000', + 'COP 10.000', + 'COP 1.000.000', + 'COP 10.000.000', + 'COP 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Riyal', () => { + const formater = currencyFormaterMap[NumberFormat.Riyal]; + + const result = [ + 'SAR 0', + 'SAR 1', + 'SAR 0.5', + 'SAR 0.57', + 'SAR 1,000', + 'SAR 10,000', + 'SAR 1,000,000', + 'SAR 10,000,000', + 'SAR 1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Ringgit', () => { + const formater = currencyFormaterMap[NumberFormat.Ringgit]; + + const result = [ + 'RM 0', + 'RM 1', + 'RM 0.5', + 'RM 0.57', + 'RM 1,000', + 'RM 10,000', + 'RM 1,000,000', + 'RM 10,000,000', + 'RM 1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Leu', () => { + const formater = currencyFormaterMap[NumberFormat.Leu]; + + const result = [ + '0 RON', + '1 RON', + '0,5 RON', + '0,57 RON', + '1.000 RON', + '10.000 RON', + '1.000.000 RON', + '10.000.000 RON', + '1.000.000 RON', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for ArgentinePeso', () => { + const formater = currencyFormaterMap[NumberFormat.ArgentinePeso]; + + const result = [ + 'ARS 0', + 'ARS 1', + 'ARS 0,5', + 'ARS 0,57', + 'ARS 1.000', + 'ARS 10.000', + 'ARS 1.000.000', + 'ARS 10.000.000', + 'ARS 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for UruguayanPeso', () => { + const formater = currencyFormaterMap[NumberFormat.UruguayanPeso]; + + const result = [ + 'UYU 0', + 'UYU 1', + 'UYU 0,5', + 'UYU 0,57', + 'UYU 1.000', + 'UYU 10.000', + 'UYU 1.000.000', + 'UYU 10.000.000', + 'UYU 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts new file mode 100644 index 0000000000..589f6ac3ec --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts @@ -0,0 +1,229 @@ +import { NumberFormat } from './number.type'; + +const commonProps = { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + style: 'currency', + currencyDisplay: 'symbol', + useGrouping: true, +}; + +export const currencyFormaterMap: Record string> = { + [NumberFormat.Num]: (n: number) => + new Intl.NumberFormat('en-US', { + style: 'decimal', + minimumFractionDigits: 0, + maximumFractionDigits: 20, + }).format(n), + [NumberFormat.Percent]: (n: number) => + new Intl.NumberFormat('en-US', { + ...commonProps, + style: 'decimal', + }).format(n) + '%', + [NumberFormat.USD]: (n: number) => + new Intl.NumberFormat('en-US', { + ...commonProps, + currency: 'USD', + }).format(n), + [NumberFormat.CanadianDollar]: (n: number) => + new Intl.NumberFormat('en-CA', { + ...commonProps, + currency: 'CAD', + }) + .format(n) + .replace('$', 'CA$'), + [NumberFormat.EUR]: (n: number) => + new Intl.NumberFormat('en-IE', { + ...commonProps, + currency: 'EUR', + }).format(n), + [NumberFormat.Pound]: (n: number) => + new Intl.NumberFormat('en-GB', { + ...commonProps, + currency: 'GBP', + }).format(n), + [NumberFormat.Yen]: (n: number) => + new Intl.NumberFormat('ja-JP', { + ...commonProps, + currency: 'JPY', + }).format(n), + [NumberFormat.Ruble]: (n: number) => + new Intl.NumberFormat('ru-RU', { + ...commonProps, + currency: 'RUB', + currencyDisplay: 'code', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Rupee]: (n: number) => + new Intl.NumberFormat('hi-IN', { + ...commonProps, + currency: 'INR', + }).format(n), + [NumberFormat.Won]: (n: number) => + new Intl.NumberFormat('ko-KR', { + ...commonProps, + currency: 'KRW', + }).format(n), + [NumberFormat.Yuan]: (n: number) => + new Intl.NumberFormat('zh-CN', { + ...commonProps, + currency: 'CNY', + }) + .format(n) + .replace('¥', 'CN¥'), + [NumberFormat.Real]: (n: number) => + new Intl.NumberFormat('pt-BR', { + ...commonProps, + currency: 'BRL', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Lira]: (n: number) => + new Intl.NumberFormat('tr-TR', { + ...commonProps, + currency: 'TRY', + currencyDisplay: 'code', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Rupiah]: (n: number) => + new Intl.NumberFormat('id-ID', { + ...commonProps, + currency: 'IDR', + currencyDisplay: 'code', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Franc]: (n: number) => + new Intl.NumberFormat('de-CH', { + ...commonProps, + currency: 'CHF', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.HongKongDollar]: (n: number) => + new Intl.NumberFormat('zh-HK', { + ...commonProps, + currency: 'HKD', + }).format(n), + [NumberFormat.NewZealandDollar]: (n: number) => + new Intl.NumberFormat('en-NZ', { + ...commonProps, + currency: 'NZD', + }) + .format(n) + .replace('$', 'NZ$'), + [NumberFormat.Krona]: (n: number) => + new Intl.NumberFormat('sv-SE', { + ...commonProps, + currency: 'SEK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.NorwegianKrone]: (n: number) => + new Intl.NumberFormat('nb-NO', { + ...commonProps, + currency: 'NOK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.MexicanPeso]: (n: number) => + new Intl.NumberFormat('es-MX', { + ...commonProps, + currency: 'MXN', + }) + .format(n) + .replace('$', 'MX$'), + [NumberFormat.Rand]: (n: number) => + new Intl.NumberFormat('en-ZA', { + ...commonProps, + currency: 'ZAR', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.NewTaiwanDollar]: (n: number) => + new Intl.NumberFormat('zh-TW', { + ...commonProps, + currency: 'TWD', + }) + .format(n) + .replace('$', 'NT$'), + [NumberFormat.DanishKrone]: (n: number) => + new Intl.NumberFormat('da-DK', { + ...commonProps, + currency: 'DKK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Baht]: (n: number) => + new Intl.NumberFormat('th-TH', { + ...commonProps, + currency: 'THB', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Forint]: (n: number) => + new Intl.NumberFormat('hu-HU', { + ...commonProps, + currency: 'HUF', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Koruna]: (n: number) => + new Intl.NumberFormat('cs-CZ', { + ...commonProps, + currency: 'CZK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Shekel]: (n: number) => + new Intl.NumberFormat('he-IL', { + ...commonProps, + currency: 'ILS', + }).format(n), + [NumberFormat.ChileanPeso]: (n: number) => + new Intl.NumberFormat('es-CL', { + ...commonProps, + currency: 'CLP', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.PhilippinePeso]: (n: number) => + new Intl.NumberFormat('fil-PH', { + ...commonProps, + currency: 'PHP', + }).format(n), + [NumberFormat.Dirham]: (n: number) => + new Intl.NumberFormat('ar-AE', { + ...commonProps, + currency: 'AED', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.ColombianPeso]: (n: number) => + new Intl.NumberFormat('es-CO', { + ...commonProps, + currency: 'COP', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Riyal]: (n: number) => + new Intl.NumberFormat('en-US', { + ...commonProps, + currency: 'SAR', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Ringgit]: (n: number) => + new Intl.NumberFormat('ms-MY', { + ...commonProps, + currency: 'MYR', + }).format(n), + [NumberFormat.Leu]: (n: number) => + new Intl.NumberFormat('ro-RO', { + ...commonProps, + currency: 'RON', + }).format(n), + [NumberFormat.ArgentinePeso]: (n: number) => + new Intl.NumberFormat('es-AR', { + ...commonProps, + currency: 'ARS', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.UruguayanPeso]: (n: number) => + new Intl.NumberFormat('es-UY', { + ...commonProps, + currency: 'UYU', + currencyDisplay: 'code', + }).format(n), +}; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts new file mode 100644 index 0000000000..27ca7cd8d8 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts @@ -0,0 +1,3 @@ +export * from './format'; +export * from './number.type'; +export * from './parse'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts new file mode 100644 index 0000000000..9140531325 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts @@ -0,0 +1,56 @@ +import { Filter } from '@/application/database-yjs'; + +export enum NumberFormat { + Num = 0, + USD = 1, + CanadianDollar = 2, + EUR = 4, + Pound = 5, + Yen = 6, + Ruble = 7, + Rupee = 8, + Won = 9, + Yuan = 10, + Real = 11, + Lira = 12, + Rupiah = 13, + Franc = 14, + HongKongDollar = 15, + NewZealandDollar = 16, + Krona = 17, + NorwegianKrone = 18, + MexicanPeso = 19, + Rand = 20, + NewTaiwanDollar = 21, + DanishKrone = 22, + Baht = 23, + Forint = 24, + Koruna = 25, + Shekel = 26, + ChileanPeso = 27, + PhilippinePeso = 28, + Dirham = 29, + ColombianPeso = 30, + Riyal = 31, + Ringgit = 32, + Leu = 33, + ArgentinePeso = 34, + UruguayanPeso = 35, + Percent = 36, +} + +export enum NumberFilterCondition { + Equal = 0, + NotEqual = 1, + GreaterThan = 2, + LessThan = 3, + GreaterThanOrEqualTo = 4, + LessThanOrEqualTo = 5, + NumberIsEmpty = 6, + NumberIsNotEmpty = 7, +} + +export interface NumberFilter extends Filter { + condition: NumberFilterCondition; + content: string; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts new file mode 100644 index 0000000000..9abac198b4 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts @@ -0,0 +1,11 @@ +import { YDatabaseField } from '@/application/collab.type'; +import { getTypeOptions } from '../type_option'; +import { NumberFormat } from './number.type'; + +export function parseNumberTypeOptions(field: YDatabaseField) { + const numberTypeOption = getTypeOptions(field)?.toJSON(); + + return { + format: parseInt(numberTypeOption.format) as NumberFormat, + }; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts new file mode 100644 index 0000000000..4b94064b52 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts @@ -0,0 +1,2 @@ +export * from './parse'; +export * from './relation.type'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts new file mode 100644 index 0000000000..c5820576cd --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts @@ -0,0 +1,9 @@ +import { YDatabaseField } from '@/application/collab.type'; +import { RelationTypeOption } from './relation.type'; +import { getTypeOptions } from '../type_option'; + +export function parseRelationTypeOption(field: YDatabaseField) { + const relationTypeOption = getTypeOptions(field)?.toJSON(); + + return relationTypeOption as RelationTypeOption; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts new file mode 100644 index 0000000000..31021afc38 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts @@ -0,0 +1,9 @@ +import { Filter } from '@/application/database-yjs'; + +export interface RelationTypeOption { + database_id: string; +} + +export interface RelationFilter extends Filter { + condition: number; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts new file mode 100644 index 0000000000..a569b2ca47 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts @@ -0,0 +1,2 @@ +export * from './select_option.type'; +export * from './parse'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts new file mode 100644 index 0000000000..7840278a34 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts @@ -0,0 +1,28 @@ +import { YDatabaseField, YjsDatabaseKey } from '@/application/collab.type'; +import { getTypeOptions } from '../type_option'; +import { SelectTypeOption } from './select_option.type'; + +export function parseSelectOptionTypeOptions(field: YDatabaseField) { + const content = getTypeOptions(field)?.get(YjsDatabaseKey.content); + + if (!content) return null; + + try { + return JSON.parse(content) as SelectTypeOption; + } catch (e) { + return null; + } +} + +export function parseSelectOptionCellData(field: YDatabaseField, data: string) { + const typeOption = parseSelectOptionTypeOptions(field); + const selectedIds = typeof data === 'string' ? data.split(',') : []; + + return selectedIds + .map((id) => { + const option = typeOption?.options?.find((option) => option.id === id); + + return option?.name ?? ''; + }) + .join(', '); +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts new file mode 100644 index 0000000000..343941d588 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts @@ -0,0 +1,38 @@ +import { Filter } from '@/application/database-yjs'; + +export enum SelectOptionColor { + Purple = 'Purple', + Pink = 'Pink', + LightPink = 'LightPink', + Orange = 'Orange', + Yellow = 'Yellow', + Lime = 'Lime', + Green = 'Green', + Aqua = 'Aqua', + Blue = 'Blue', +} + +export enum SelectOptionFilterCondition { + OptionIs = 0, + OptionIsNot = 1, + OptionContains = 2, + OptionDoesNotContain = 3, + OptionIsEmpty = 4, + OptionIsNotEmpty = 5, +} + +export interface SelectOptionFilter extends Filter { + condition: SelectOptionFilterCondition; + optionIds: string[]; +} + +export interface SelectOption { + id: string; + name: string; + color: SelectOptionColor; +} + +export interface SelectTypeOption { + disable_color: boolean; + options: SelectOption[]; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts new file mode 100644 index 0000000000..7d0a52cd9d --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts @@ -0,0 +1 @@ +export * from './text.type'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts new file mode 100644 index 0000000000..c2f230c738 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts @@ -0,0 +1,17 @@ +import { Filter } from '@/application/database-yjs'; + +export enum TextFilterCondition { + TextIs = 0, + TextIsNot = 1, + TextContains = 2, + TextDoesNotContain = 3, + TextStartsWith = 4, + TextEndsWith = 5, + TextIsEmpty = 6, + TextIsNotEmpty = 7, +} + +export interface TextFilter extends Filter { + condition: TextFilterCondition; + content: string; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts new file mode 100644 index 0000000000..bf9c80706f --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts @@ -0,0 +1,8 @@ +import { YDatabaseField, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs'; + +export function getTypeOptions(field: YDatabaseField) { + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + return field?.get(YjsDatabaseKey.type_option)?.get(String(fieldType)); +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/filter.ts b/frontend/appflowy_web_app/src/application/database-yjs/filter.ts new file mode 100644 index 0000000000..73a8663371 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/filter.ts @@ -0,0 +1,223 @@ +import { + YDatabaseFields, + YDatabaseFilter, + YDatabaseFilters, + YDatabaseRow, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { + CheckboxFilter, + CheckboxFilterCondition, + ChecklistFilter, + ChecklistFilterCondition, + DateFilter, + NumberFilter, + NumberFilterCondition, + parseChecklistData, + SelectOptionFilter, + SelectOptionFilterCondition, + TextFilter, + TextFilterCondition, +} from '@/application/database-yjs/fields'; +import { Row } from '@/application/database-yjs/selector'; +import Decimal from 'decimal.js'; +import * as Y from 'yjs'; +import { every, filter, some } from 'lodash-es'; + +export function parseFilter(fieldType: FieldType, filter: YDatabaseFilter) { + const fieldId = filter.get(YjsDatabaseKey.field_id); + const filterType = Number(filter.get(YjsDatabaseKey.filter_type)); + const id = filter.get(YjsDatabaseKey.id); + const content = filter.get(YjsDatabaseKey.content); + const condition = Number(filter.get(YjsDatabaseKey.condition)); + + const value = { + fieldId, + filterType, + condition, + id, + content, + }; + + switch (fieldType) { + case FieldType.URL: + case FieldType.RichText: + return value as TextFilter; + case FieldType.Number: + return value as NumberFilter; + case FieldType.Checklist: + return value as ChecklistFilter; + case FieldType.Checkbox: + return value as CheckboxFilter; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + // eslint-disable-next-line no-case-declarations + const options = content.split(','); + + return { + ...value, + optionIds: options, + } as SelectOptionFilter; + case FieldType.DateTime: + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return value as DateFilter; + } + + return value; +} + +function createPredicate(conditions: ((row: Row) => boolean)[]) { + return function (item: Row) { + return every(conditions, (condition) => condition(item)); + }; +} + +export function filterBy(rows: Row[], filters: YDatabaseFilters, fields: YDatabaseFields, rowMetas: Y.Map) { + const filterArray = filters.toArray(); + const conditions = filterArray.map((filter) => { + return (row: { id: string }) => { + const fieldId = filter.get(YjsDatabaseKey.field_id); + const field = fields.get(fieldId); + const fieldType = Number(field.get(YjsDatabaseKey.type)); + const rowId = row.id; + const rowMeta = rowMetas.get(rowId); + + if (!rowMeta) return false; + const filterValue = parseFilter(fieldType, filter); + const meta = rowMeta.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + if (!meta) return false; + + const cells = meta.get(YjsDatabaseKey.cells); + const cell = cells.get(fieldId); + + if (!cell) return false; + const { condition, content } = filterValue; + + switch (fieldType) { + case FieldType.URL: + case FieldType.RichText: + return textFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + case FieldType.Number: + return numberFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + case FieldType.Checkbox: + return checkboxFilterCheck(cell.get(YjsDatabaseKey.data) as string, condition); + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return selectOptionFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + case FieldType.Checklist: + return checklistFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + default: + return true; + } + }; + }); + const predicate = createPredicate(conditions); + + return filter(rows, predicate); +} + +export function textFilterCheck(data: string, content: string, condition: TextFilterCondition) { + switch (condition) { + case TextFilterCondition.TextContains: + return data.includes(content); + case TextFilterCondition.TextDoesNotContain: + return !data.includes(content); + case TextFilterCondition.TextIs: + return data === content; + case TextFilterCondition.TextIsNot: + return data !== content; + case TextFilterCondition.TextIsEmpty: + return data === ''; + case TextFilterCondition.TextIsNotEmpty: + return data !== ''; + default: + return false; + } +} + +export function numberFilterCheck(data: string, content: string, condition: number) { + const decimal = new Decimal(data).toNumber(); + const filterDecimal = new Decimal(content).toNumber(); + + switch (condition) { + case NumberFilterCondition.Equal: + return decimal === filterDecimal; + case NumberFilterCondition.NotEqual: + return decimal !== filterDecimal; + case NumberFilterCondition.GreaterThan: + return decimal > filterDecimal; + case NumberFilterCondition.GreaterThanOrEqualTo: + return decimal >= filterDecimal; + case NumberFilterCondition.LessThan: + return decimal < filterDecimal; + case NumberFilterCondition.LessThanOrEqualTo: + return decimal <= filterDecimal; + case NumberFilterCondition.NumberIsEmpty: + return data === ''; + case NumberFilterCondition.NumberIsNotEmpty: + return data !== ''; + default: + return false; + } +} + +export function checkboxFilterCheck(data: string, condition: number) { + switch (condition) { + case CheckboxFilterCondition.IsChecked: + return data === 'Yes'; + case CheckboxFilterCondition.IsUnChecked: + return data !== 'Yes'; + default: + return false; + } +} + +export function checklistFilterCheck(data: string, content: string, condition: number) { + const percentage = parseChecklistData(data)?.percentage ?? 0; + + if (condition === ChecklistFilterCondition.IsComplete) { + return percentage === 100; + } + + return percentage !== 100; +} + +export function selectOptionFilterCheck(data: string, content: string, condition: number) { + const selectedOptionIds = data.split(','); + const filterOptionIds = content.split(','); + + switch (condition) { + // Ensure all filterOptionIds are included in selectedOptionIds + case SelectOptionFilterCondition.OptionIs: + return every(filterOptionIds, (option) => selectedOptionIds.includes(option)); + + // Ensure none of the filterOptionIds are included in selectedOptionIds + case SelectOptionFilterCondition.OptionIsNot: + return every(filterOptionIds, (option) => !selectedOptionIds.includes(option)); + + // Ensure at least one of the filterOptionIds is included in selectedOptionIds + case SelectOptionFilterCondition.OptionContains: + return some(filterOptionIds, (option) => selectedOptionIds.includes(option)); + + // Ensure at least one of the filterOptionIds is not included in selectedOptionIds + case SelectOptionFilterCondition.OptionDoesNotContain: + return some(filterOptionIds, (option) => !selectedOptionIds.includes(option)); + + // Ensure selectedOptionIds is empty + case SelectOptionFilterCondition.OptionIsEmpty: + return selectedOptionIds.length === 0; + + // Ensure selectedOptionIds is not empty + case SelectOptionFilterCondition.OptionIsNotEmpty: + return selectedOptionIds.length !== 0; + + // Default case, if no conditions match + default: + return false; + } +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/index.ts new file mode 100644 index 0000000000..708ae080d2 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/index.ts @@ -0,0 +1,8 @@ +export * from './context'; +export * from './fields'; +export * from './context'; +export * from './selector'; +export * from './database.type'; +export * from './const'; +export * from './filter'; +export * from './sort'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts new file mode 100644 index 0000000000..c3222fdf65 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts @@ -0,0 +1,227 @@ +import { FieldId, SortId, YDatabaseField, YjsDatabaseKey } from '@/application/collab.type'; +import { MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; +import { useDatabase, useGridRows, useViewId } from '@/application/database-yjs/context'; +import { parseFilter } from '@/application/database-yjs/filter'; +import { FieldType, FieldVisibility, Filter, SortCondition } from './database.type'; +import { useEffect, useMemo, useState } from 'react'; + +export interface Column { + fieldId: string; + width: number; + visibility: FieldVisibility; + wrap?: boolean; +} + +export interface Row { + id: string; + height: number; +} + +const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty]; + +export function useGridColumnsSelector(viewId: string, visibilitys: FieldVisibility[] = defaultVisible) { + const database = useDatabase(); + const [columns, setColumns] = useState([]); + + useEffect(() => { + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const fields = database?.get(YjsDatabaseKey.fields); + const fieldsOrder = view?.get(YjsDatabaseKey.field_orders); + const fieldSettings = view?.get(YjsDatabaseKey.field_settings); + const getColumns = () => { + if (!fields || !fieldsOrder || !fieldSettings) return []; + const fieldIds = fieldsOrder.toJSON().map((item) => item.id) as string[]; + + return fieldIds + .map((fieldId) => { + const setting = fieldSettings.get(fieldId); + + return { + fieldId, + width: parseInt(setting?.get(YjsDatabaseKey.width)) || MIN_COLUMN_WIDTH, + visibility: parseInt(setting?.get(YjsDatabaseKey.visibility)) as FieldVisibility, + wrap: setting?.get(YjsDatabaseKey.wrap), + }; + }) + .filter((column) => visibilitys.includes(column.visibility)); + }; + + const observerEvent = () => setColumns(getColumns()); + + setColumns(getColumns()); + + fieldsOrder?.observe(observerEvent); + fieldSettings?.observe(observerEvent); + + return () => { + fieldsOrder?.unobserve(observerEvent); + fieldSettings?.unobserve(observerEvent); + }; + }, [database, viewId, visibilitys]); + + return columns; +} + +export function useGridRowsSelector() { + const rowOrders = useGridRows(); + + return useMemo(() => rowOrders ?? [], [rowOrders]); +} + +export function useFieldSelector(fieldId: string) { + const database = useDatabase(); + const [field, setField] = useState(null); + const [clock, setClock] = useState(0); + + useEffect(() => { + if (!database) return; + + const field = database.get(YjsDatabaseKey.fields)?.get(fieldId); + + setField(field || null); + const observerEvent = () => setClock((prev) => prev + 1); + + field.observe(observerEvent); + + return () => { + field.unobserve(observerEvent); + }; + }, [database, fieldId]); + + return { + field, + clock, + }; +} + +export function useFiltersSelector() { + const database = useDatabase(); + const viewId = useViewId(); + const [filters, setFilters] = useState([]); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const filterOrders = view?.get(YjsDatabaseKey.filters); + + if (!filterOrders) return; + + const getFilters = () => { + return filterOrders.toJSON().map((item) => item.id); + }; + + const observerEvent = () => setFilters(getFilters()); + + setFilters(getFilters()); + + filterOrders.observe(observerEvent); + + return () => { + filterOrders.unobserve(observerEvent); + }; + }, [database, viewId]); + + return filters; +} + +export function useFilterSelector(filterId: string) { + const database = useDatabase(); + const viewId = useViewId(); + const fields = database?.get(YjsDatabaseKey.fields); + const [filterValue, setFilterValue] = useState(null); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const filter = view + ?.get(YjsDatabaseKey.filters) + .toArray() + .find((filter) => filter.get(YjsDatabaseKey.id) === filterId); + const field = fields?.get(filter?.get(YjsDatabaseKey.field_id) as FieldId); + + const observerEvent = () => { + if (!filter || !field) return; + const fieldType = Number(field.get(YjsDatabaseKey.type)) as FieldType; + + setFilterValue(parseFilter(fieldType, filter)); + }; + + observerEvent(); + field?.observe(observerEvent); + filter?.observe(observerEvent); + return () => { + field?.unobserve(observerEvent); + filter?.unobserve(observerEvent); + }; + }, [fields, viewId, filterId, database]); + return filterValue; +} + +export function useSortsSelector() { + const database = useDatabase(); + const viewId = useViewId(); + const [sorts, setSorts] = useState([]); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const sortOrders = view?.get(YjsDatabaseKey.sorts); + + if (!sortOrders) return; + + const getSorts = () => { + return sortOrders.toJSON().map((item) => item.id); + }; + + const observerEvent = () => setSorts(getSorts()); + + setSorts(getSorts()); + + sortOrders.observe(observerEvent); + + return () => { + sortOrders.unobserve(observerEvent); + }; + }, [database, viewId]); + + return sorts; +} + +export interface Sort { + fieldId: FieldId; + condition: SortCondition; + id: SortId; +} + +export function useSortSelector(sortId: SortId) { + const database = useDatabase(); + const viewId = useViewId(); + const [sortValue, setSortValue] = useState(null); + const views = database?.get(YjsDatabaseKey.views); + + useEffect(() => { + if (!viewId) return; + const view = views?.get(viewId); + const sort = view + ?.get(YjsDatabaseKey.sorts) + .toArray() + .find((sort) => sort.get(YjsDatabaseKey.id) === sortId); + + const observerEvent = () => { + setSortValue({ + fieldId: sort?.get(YjsDatabaseKey.field_id) as FieldId, + condition: Number(sort?.get(YjsDatabaseKey.condition)), + id: sort?.get(YjsDatabaseKey.id) as SortId, + }); + }; + + observerEvent(); + sort?.observe(observerEvent); + + return () => { + sort?.unobserve(observerEvent); + }; + }, [viewId, sortId, views]); + + return sortValue; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/sort.ts b/frontend/appflowy_web_app/src/application/database-yjs/sort.ts new file mode 100644 index 0000000000..355d4b4ad9 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/sort.ts @@ -0,0 +1,79 @@ +import { + YDatabaseField, + YDatabaseFields, + YDatabaseRow, + YDatabaseSorts, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; +import { FieldType, SortCondition } from '@/application/database-yjs/database.type'; +import { parseChecklistData, parseSelectOptionCellData } from '@/application/database-yjs/fields'; +import { Row } from '@/application/database-yjs/selector'; +import orderBy from 'lodash-es/orderBy'; +import * as Y from 'yjs'; + +export function sortBy(rows: Row[], sorts: YDatabaseSorts, fields: YDatabaseFields, rowMetas: Y.Map) { + const sortArray = sorts.toArray(); + const iteratees = sortArray.map((sort) => { + return (row: { id: string }) => { + const fieldId = sort.get(YjsDatabaseKey.field_id); + const field = fields.get(fieldId); + const fieldType = Number(field.get(YjsDatabaseKey.type)); + + const rowId = row.id; + const rowMeta = rowMetas.get(rowId); + + const defaultData = parseCellDataForSort(field, ''); + + if (!rowMeta) return defaultData; + const meta = rowMeta.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + if (!meta) return defaultData; + if (fieldType === FieldType.LastEditedTime) { + return meta.get(YjsDatabaseKey.last_modified); + } + + if (fieldType === FieldType.CreatedTime) { + return meta.get(YjsDatabaseKey.created_at); + } + + const cells = meta.get(YjsDatabaseKey.cells); + const cell = cells.get(fieldId); + + if (!cell) return defaultData; + + return parseCellDataForSort(field, cell.get(YjsDatabaseKey.data) ?? ''); + }; + }); + const orders = sortArray.map((sort) => { + const condition = Number(sort.get(YjsDatabaseKey.condition)); + + if (condition === SortCondition.Descending) return 'desc'; + return 'asc'; + }); + + return orderBy(rows, iteratees, orders); +} + +export function parseCellDataForSort(field: YDatabaseField, data: string | boolean | number | object) { + const fieldType = Number(field.get(YjsDatabaseKey.type)); + + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + case FieldType.Number: + return data; + case FieldType.Checkbox: + return data === 'Yes'; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return parseSelectOptionCellData(field, typeof data === 'string' ? data : ''); + case FieldType.Checklist: + return parseChecklistData(typeof data === 'string' ? data : '')?.percentage ?? 0; + case FieldType.DateTime: + return Number(data); + case FieldType.Relation: + return ''; + } +} diff --git a/frontend/appflowy_web_app/src/application/document.type.ts b/frontend/appflowy_web_app/src/application/document.type.ts deleted file mode 100644 index da559c5bde..0000000000 --- a/frontend/appflowy_web_app/src/application/document.type.ts +++ /dev/null @@ -1,176 +0,0 @@ -import Y from 'yjs'; - -export type BlockId = string; - -export type ExternalId = string; - -export type ChildrenId = string; - -export enum BlockType { - Paragraph = 'paragraph', - Page = 'page', - HeadingBlock = 'heading', - TodoListBlock = 'todo_list', - BulletedListBlock = 'bulleted_list', - NumberedListBlock = 'numbered_list', - ToggleListBlock = 'toggle_list', - CodeBlock = 'code', - EquationBlock = 'math_equation', - QuoteBlock = 'quote', - CalloutBlock = 'callout', - DividerBlock = 'divider', - ImageBlock = 'image', - GridBlock = 'grid', - OutlineBlock = 'outline', - TableBlock = 'table', - TableCell = 'table/cell', -} - -export enum InlineBlockType { - Formula = 'formula', - Mention = 'mention', -} - -export enum AlignType { - Left = 'left', - Center = 'center', - Right = 'right', -} - -export interface BlockData { - bg_color?: string; - font_color?: string; - align?: AlignType; -} - -export interface HeadingBlockData extends BlockData { - level: number; -} - -export interface NumberedListBlockData extends BlockData { - number: number; -} - -export interface TodoListBlockData extends BlockData { - checked: boolean; -} - -export interface ToggleListBlockData extends BlockData { - collapsed: boolean; -} - -export interface CodeBlockData extends BlockData { - language: string; -} - -export interface CalloutBlockData extends BlockData { - icon: string; -} - -export interface MathEquationBlockData extends BlockData { - formula?: string; -} - -export enum ImageType { - Local = 0, - Internal = 1, - External = 2, -} - -export interface ImageBlockData extends BlockData { - url?: string; - width?: number; - align?: AlignType; - image_type?: ImageType; - height?: number; -} - -export interface OutlineBlockData extends BlockData { - depth?: number; -} - -export interface TableBlockData extends BlockData { - colDefaultWidth: number; - colMinimumWidth: number; - colsHeight: number; - colsLen: number; - rowDefaultHeight: number; - rowsLen: number; -} - -export interface TableCellBlockData extends BlockData { - colPosition: number; - height: number; - rowPosition: number; - width: number; -} - -export enum MentionType { - PageRef = 'page', - Date = 'date', -} - -export interface Mention { - // inline page ref id - page_id?: string; - // reminder date ref id - date?: string; - - type: MentionType; -} - -export enum YjsEditorKey { - data_section = 'data', - document = 'document', - database = 'database', - workspace_database = 'databases', - folder = 'folder', - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - database_row = 'data', - user_awareness = 'user_awareness', - blocks = 'blocks', - page_id = 'page_id', - meta = 'meta', - children_map = 'children_map', - text_map = 'text_map', - text = 'text', - delta = 'delta', - - block_id = 'id', - block_type = 'ty', - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - block_data = 'data', - block_parent = 'parent', - block_children = 'children', - block_external_id = 'external_id', - block_external_type = 'external_type', -} - -export interface YDoc extends Y.Doc { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - get(key: YjsEditorKey.data_section | string): YSharedRoot | any; -} - -export interface YSharedRoot extends Y.Map { - get(key: YjsEditorKey.document): YDocument; -} - -export interface YDocument extends Y.Map { - get(key: YjsEditorKey.blocks | YjsEditorKey.page_id | YjsEditorKey.meta): YBlocks | YMeta | string; -} - -export interface YBlocks extends Y.Map { - get(key: BlockId): Y.Map; -} - -export interface YMeta extends Y.Map { - get(key: YjsEditorKey.children_map | YjsEditorKey.text_map): YChildrenMap | YTextMap; -} - -export interface YChildrenMap extends Y.Map { - get(key: ChildrenId): Y.Array; -} - -export interface YTextMap extends Y.Map { - get(key: ExternalId): Y.Text; -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts new file mode 100644 index 0000000000..a1bfcdbf21 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts @@ -0,0 +1,170 @@ +import { CollabOrigin, CollabType, YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { + batchCollabs, + getCollabStorage, + getCollabStorageWithAPICall, + getUserWorkspace, +} from '@/application/services/js-services/storage'; +import { DatabaseService } from '@/application/services/services.type'; +import * as Y from 'yjs'; + +export class JSDatabaseService implements DatabaseService { + private loadedDatabaseId: Set = new Set(); + + constructor() { + // + } + + async getDatabase( + workspaceId: string, + databaseId: string + ): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + const rootRowsDoc = new Y.Doc(); + const rowsFolder = rootRowsDoc.getMap(); + const isLoaded = this.loadedDatabaseId.has(databaseId); + let databaseDoc: YDoc | undefined = undefined; + + if (isLoaded) { + databaseDoc = (await getCollabStorage(databaseId, CollabType.Database)).doc; + } else { + databaseDoc = await getCollabStorageWithAPICall(workspaceId, databaseId, CollabType.Database); + } + + const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase; + const viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString(); + const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders); + const rowIds = rowOrders.toJSON() as { + id: string; + }[]; + + if (!rowIds) { + throw new Error('Database rows not found'); + } + + if (isLoaded) { + for (const row of rowIds) { + const { doc } = await getCollabStorage(row.id, CollabType.DatabaseRow); + + rowsFolder.set(row.id, doc); + } + } else { + const rows = await this.loadDatabaseRows( + workspaceId, + rowIds.map((item) => item.id) + ); + + rows.forEach((row, id) => { + rowsFolder.set(id, row); + }); + } + + this.loadedDatabaseId.add(databaseId); + + return { + databaseDoc, + rows: rowsFolder as Y.Map, + }; + } + + async openDatabase( + workspaceId: string, + viewId: string + ): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + const userWorkspace = await getUserWorkspace(); + + if (!userWorkspace) { + throw new Error('User workspace not found'); + } + + const workspaceDatabaseId = userWorkspace.workspaces.find( + (workspace) => workspace.id === workspaceId + )?.workspaceDatabaseId; + + if (!workspaceDatabaseId) { + throw new Error('Workspace database not found'); + } + + const workspaceDatabase = await getCollabStorageWithAPICall( + workspaceId, + workspaceDatabaseId, + CollabType.WorkspaceDatabase + ); + + const databases = workspaceDatabase + .getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.workspace_database) + .toJSON() as { + views: string[]; + database_id: string; + }[]; + + const databaseMeta = databases.find((item) => { + return item.views.some((databaseViewId: string) => databaseViewId === viewId); + }); + + if (!databaseMeta) { + throw new Error('Database not found'); + } + + const { databaseDoc, rows } = await this.getDatabase(workspaceId, databaseMeta.database_id); + const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase; + const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders); + + // Update rows if new rows are added + rowOrders?.observe((event) => { + if (event.changes.added.size > 0) { + const rowIds = rowOrders.toJSON() as { + id: string; + }[]; + + console.log('Update rows', rowIds); + void this.loadDatabaseRows( + workspaceId, + rowIds.map((item) => item.id) + ).then((newRows) => { + newRows.forEach((row, id) => { + rows.set(id, row); + }); + }); + } + }); + const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { + if (origin === CollabOrigin.LocalSync) { + // Send the update to the server + console.log('update', update); + } + }; + + databaseDoc.on('update', handleUpdate); + + return { + databaseDoc, + rows, + }; + } + + async loadDatabaseRows(workspaceId: string, rowIds: string[]) { + const rows = new Map(); + + try { + await batchCollabs( + workspaceId, + rowIds.map((id) => ({ + object_id: id, + collab_type: CollabType.DatabaseRow, + })), + (id, rowDoc) => rows.set(id, rowDoc) + ); + } catch (e) { + console.error(e); + } + + return rows; + } +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/db/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/db/index.ts index ebe8870c15..bf5f0c7aa1 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/db/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/db/index.ts @@ -1,41 +1,8 @@ import { YDoc } from '@/application/collab.type'; -import { getAuthInfo } from '@/application/services/js-services/storage'; -import * as Y from 'yjs'; -import { IndexeddbPersistence } from 'y-indexeddb'; import { databasePrefix } from '@/application/constants'; -import BaseDexie from 'dexie'; -import { usersSchema, UsersTable } from './tables/users'; - -const version = 1; - -type DexieTables = UsersTable; -export type Dexie = BaseDexie & T; - -let db: Dexie | undefined; - -export function getDB() { - const authInfo = getAuthInfo(); - - if (!db && authInfo?.uuid) { - return openDB(authInfo?.uuid); - } - - return db; -} - -export function openDB(uuid: string) { - const dbName = `${databasePrefix}_${uuid}`; - - if (db && db.name === dbName) { - return db; - } - - db = new BaseDexie(dbName) as Dexie; - const schema = Object.assign({}, usersSchema); - - db.version(version).stores(schema); - return db; -} +import { getAuthInfo } from '@/application/services/js-services/storage'; +import { IndexeddbPersistence } from 'y-indexeddb'; +import * as Y from 'yjs'; /** * Open the collaboration database, and return a function to close it @@ -66,3 +33,10 @@ export async function deleteCollabDB(docName: string) { await provider.destroy(); } + +export function getDBName(id: string, type: string) { + const { uuid } = getAuthInfo() || {}; + + if (!uuid) throw new Error('No user found'); + return `${uuid}_${type}_${id}`; +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/db/tables/users.ts b/frontend/appflowy_web_app/src/application/services/js-services/db/tables/users.ts deleted file mode 100644 index 1da8f20b0c..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/db/tables/users.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Table } from 'dexie'; -import { UserProfile } from '@/application/user.type'; - -export type UsersTable = { - users: Table; -}; - -export const usersSchema = { - users: 'uuid, uid, email, name, workspaceId, iconUrl', -}; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts index 1af92df8a0..e93809449d 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts @@ -1,41 +1,20 @@ import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type'; -import { getDocumentStorage } from '@/application/services/js-services/storage/document'; +import { getCollabStorageWithAPICall } from '@/application/services/js-services/storage'; import { DocumentService } from '@/application/services/services.type'; -import { APIService } from 'src/application/services/js-services/wasm'; -import { applyDocument } from 'src/application/ydoc/apply'; export class JSDocumentService implements DocumentService { constructor() { // } - fetchDocument(workspaceId: string, docId: string) { - return APIService.getCollab(workspaceId, docId, CollabType.Document); - } - async openDocument(workspaceId: string, docId: string): Promise { - const { doc, localExist } = await getDocumentStorage(docId); - const asyncApply = async () => { - const res = await this.fetchDocument(workspaceId, docId); - - applyDocument(doc, res.state); - }; - - // If the document exists locally, apply the state asynchronously, - // otherwise, apply the state synchronously - if (localExist) { - void asyncApply(); - } else { - await asyncApply(); - } + const doc = await getCollabStorageWithAPICall(workspaceId, docId, CollabType.Document); const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { - if (origin === CollabOrigin.Remote) { - return; + if (origin === CollabOrigin.LocalSync) { + // Send the update to the server + console.log('update', update); } - - // Send the update to the server - console.log('update', update); }; doc.on('update', handleUpdate); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts index 796cd078d6..c475cfa935 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts @@ -1,41 +1,19 @@ import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type'; -import { getFolderStorage } from '@/application/services/js-services/storage/folder'; +import { getCollabStorageWithAPICall } from '@/application/services/js-services/storage'; import { FolderService } from '@/application/services/services.type'; -import { APIService } from 'src/application/services/js-services/wasm'; -import { applyDocument } from 'src/application/ydoc/apply'; export class JSFolderService implements FolderService { constructor() { // } - fetchFolder(workspaceId: string) { - return APIService.getCollab(workspaceId, workspaceId, CollabType.Folder); - } - async openWorkspace(workspaceId: string): Promise { - const { doc, localExist } = await getFolderStorage(workspaceId); - const asyncApply = async () => { - const res = await this.fetchFolder(workspaceId); - - applyDocument(doc, res.state); - }; - - // If the document exists locally, apply the state asynchronously, - // otherwise, apply the state synchronously - if (localExist) { - void asyncApply(); - } else { - await asyncApply(); - } - + const doc = await getCollabStorageWithAPICall(workspaceId, workspaceId, CollabType.Folder); const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { - if (origin === CollabOrigin.Remote) { - return; + if (origin === CollabOrigin.LocalSync) { + // Send the update to the server + console.log('update', update); } - - // Send the update to the server - console.log('update', update); }; doc.on('update', handleUpdate); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/index.ts index 3410c8d27e..d31b7f117a 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/index.ts @@ -1,7 +1,9 @@ +import { JSDatabaseService } from '@/application/services/js-services/database.service'; import { AFService, AFServiceConfig, AuthService, + DatabaseService, DocumentService, FolderService, UserService, @@ -22,6 +24,8 @@ export class AFClientService implements AFService { folderService: FolderService; + databaseService: DatabaseService; + private deviceId: string = nanoid(8); private clientId: string = 'web'; @@ -45,5 +49,6 @@ export class AFClientService implements AFService { this.userService = new JSUserService(); this.documentService = new JSDocumentService(); this.folderService = new JSFolderService(); + this.databaseService = new JSDatabaseService(); } } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/auth.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/auth.ts index bb19f590bc..dd8d3d1d99 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/auth.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/storage/auth.ts @@ -1,11 +1,3 @@ -import { getAuthInfo } from '@/application/services/js-services/storage/token'; -import { openDB } from '@/application/services/js-services/db'; - export async function signInSuccess() { - const authInfo = getAuthInfo(); - - if (authInfo) { - // Open the database - openDB(authInfo.uuid); - } + // Do nothing } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts new file mode 100644 index 0000000000..27ce771d74 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts @@ -0,0 +1,101 @@ +import { CollabType, YDoc, YjsEditorKey } from '@/application/collab.type'; +import { getDBName, openCollabDB } from '@/application/services/js-services/db'; +import { APIService } from '@/application/services/js-services/wasm'; +import { applyDocument } from '@/application/ydoc/apply'; + +export function fetchCollab(workspaceId: string, id: string, type: CollabType) { + return APIService.getCollab(workspaceId, id, type); +} + +export function batchFetchCollab(workspaceId: string, params: { object_id: string; collab_type: CollabType }[]) { + return APIService.batchGetCollab(workspaceId, params); +} + +function collabTypeToDBType(type: CollabType) { + switch (type) { + case CollabType.Folder: + return 'folder'; + case CollabType.Document: + return 'document'; + case CollabType.Database: + return 'database'; + case CollabType.WorkspaceDatabase: + return 'databases'; + case CollabType.DatabaseRow: + return 'database_row'; + case CollabType.UserAwareness: + return 'user_awareness'; + default: + return ''; + } +} + +export async function getCollabStorage(id: string, type: CollabType) { + const name = getDBName(id, collabTypeToDBType(type)); + + const doc = await openCollabDB(name); + const localExist = doc.share.has(YjsEditorKey.data_section); + + return { + doc, + localExist, + }; +} + +export async function getCollabStorageWithAPICall(workspaceId: string, id: string, type: CollabType) { + const { doc, localExist } = await getCollabStorage(id, type); + const asyncApply = async () => { + const res = await fetchCollab(workspaceId, id, type); + + applyDocument(doc, res.state); + }; + + // If the document exists locally, apply the state asynchronously, + // otherwise, apply the state synchronously + if (localExist) { + void asyncApply(); + } else { + await asyncApply(); + } + + return doc; +} + +export async function batchCollabs( + workspaceId: string, + params: { + object_id: string; + collab_type: CollabType; + }[], + rowCallback?: (id: string, doc: YDoc) => void +) { + console.log('Fetching collab data:', params); + // Create or get Y.Doc from local storage + for (const item of params) { + const { object_id, collab_type } = item; + + const { doc } = await getCollabStorage(object_id, collab_type); + + if (rowCallback) { + rowCallback(object_id, doc); + } + } + + // Async fetch collab data and apply to Y.Doc + void (async () => { + const res = await batchFetchCollab(workspaceId, params); + + for (const id of Object.keys(res)) { + const type = params.find((param) => param.object_id === id)?.collab_type; + const data = res[id]; + + if (type === undefined || !data) { + continue; + } + + const { doc } = await getCollabStorage(id, type); + + applyDocument(doc, data); + } + })(); +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/document.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/document.ts deleted file mode 100644 index 0c1278d216..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/document.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { YjsEditorKey } from '@/application/collab.type'; -import { openCollabDB } from '@/application/services/js-services/db'; -import { getAuthInfo } from '@/application/services/js-services/storage/token'; - -export async function getDocumentStorage(docId: string) { - const docName = getDocName(docId); - const doc = await openCollabDB(docName); - const localExist = doc.share.has(YjsEditorKey.data_section); - - return { - doc, - localExist, - }; -} - -export function getDocName(docId: string) { - const { uuid } = getAuthInfo() || {}; - - if (!uuid) throw new Error('No user found'); - return `${uuid}_document_${docId}`; -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/folder.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/folder.ts deleted file mode 100644 index 8d70df8d0a..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/folder.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { YjsEditorKey } from '@/application/collab.type'; -import { openCollabDB } from '@/application/services/js-services/db'; -import { getAuthInfo } from '@/application/services/js-services/storage/token'; - -export async function getFolderStorage(workspaceId: string) { - const docName = getDocName(workspaceId); - const doc = await openCollabDB(docName); - const localExist = doc.share.has(YjsEditorKey.data_section); - - return { - doc, - localExist, - }; -} - -export function getDocName(workspaceId: string) { - const { uuid } = getAuthInfo() || {}; - - if (!uuid) throw new Error('No user found'); - return `${uuid}_folder_${workspaceId}`; -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/index.ts index d983c71b07..f0b9cab2d6 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/storage/index.ts @@ -1,2 +1,4 @@ -export * from './token'; -export * from './user'; \ No newline at end of file +export * from './token'; +export * from './user'; +export * from './collab'; +export * from './auth'; diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/user.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/user.ts index 0194bb8e0f..db9626ae8e 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/user.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/storage/user.ts @@ -1,18 +1,36 @@ -import { UserProfile } from '@/application/user.type'; -import { getDB } from '@/application/services/js-services/db'; -import { getAuthInfo } from '@/application/services/js-services/storage/token'; +import { UserProfile, UserWorkspace } from '@/application/user.type'; -const primaryKeyName = 'uid'; +const userKey = 'user'; +const workspaceKey = 'workspace'; export async function getSignInUser(): Promise { - const db = getDB(); - const authInfo = getAuthInfo(); + const userStr = localStorage.getItem(userKey); - return db?.users.get(authInfo?.uuid); + try { + return userStr ? JSON.parse(userStr) : undefined; + } catch (e) { + return undefined; + } } export async function setSignInUser(profile: UserProfile) { - const db = getDB(); + const userStr = JSON.stringify(profile); - return db?.users.put(profile, primaryKeyName); + localStorage.setItem(userKey, userStr); +} + +export async function getUserWorkspace(): Promise { + const str = localStorage.getItem(workspaceKey); + + try { + return str ? JSON.parse(str) : undefined; + } catch (e) { + return undefined; + } +} + +export async function setUserWorkspace(workspace: UserWorkspace) { + const str = JSON.stringify(workspace); + + localStorage.setItem(workspaceKey, str); } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts index 88e8ba996a..c4853f850d 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts @@ -1,7 +1,14 @@ import { UserService } from '@/application/services/services.type'; -import { UserProfile } from '@/application/user.type'; +import { UserProfile, UserWorkspace } from '@/application/user.type'; import { APIService } from 'src/application/services/js-services/wasm'; -import { getAuthInfo, getSignInUser, invalidToken, setSignInUser } from '@/application/services/js-services/storage'; +import { + getAuthInfo, + getSignInUser, + getUserWorkspace, + invalidToken, + setSignInUser, + setUserWorkspace, +} from '@/application/services/js-services/storage'; import { asyncDataDecorator } from '@/application/services/js-services/decorator'; async function getUser() { @@ -22,10 +29,17 @@ export class JSUserService implements UserService { return Promise.reject('Not authenticated'); } + await this.getUserWorkspace(); + return null!; } async checkUser(): Promise { return (await getSignInUser()) !== undefined; } + + @asyncDataDecorator(getUserWorkspace, setUserWorkspace, APIService.getUserWorkspace) + async getUserWorkspace(): Promise { + return null!; + } } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts index 48a76d1837..f3fecb1215 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts @@ -1,6 +1,6 @@ import { CollabType } from '@/application/collab.type'; import { ClientAPI } from '@appflowyinc/client-api-wasm'; -import { UserProfile } from '@/application/user.type'; +import { UserProfile, UserWorkspace } from '@/application/user.type'; import { AFCloudConfig } from '@/application/services/services.type'; import { invalidToken, readTokenStr, writeToken } from '@/application/services/js-services/storage'; @@ -77,3 +77,45 @@ export async function getCollab(workspaceId: string, object_id: string, collabTy state, }; } + +export async function batchGetCollab( + workspaceId: string, + params: { + object_id: string; + collab_type: CollabType; + }[] +) { + const res = (await client.batch_get_collab( + workspaceId, + params.map((param) => ({ + object_id: param.object_id, + collab_type: Number(param.collab_type) as 0 | 1 | 2 | 3 | 4 | 5, + })) + )) as unknown as Map; + + const result: Record = {}; + + res.forEach((value, key) => { + result[key] = new Uint8Array(value.doc_state); + }); + return result; +} + +export async function getUserWorkspace(): Promise { + const res = await client.get_user_workspace(); + + return { + visitingWorkspaceId: res.visiting_workspace_id, + workspaces: res.workspaces.map((workspace) => ({ + id: workspace.workspace_id, + name: workspace.workspace_name, + icon: workspace.icon, + owner: { + id: Number(workspace.owner_uid), + name: workspace.owner_name, + }, + type: workspace.workspace_type, + workspaceDatabaseId: workspace.database_storage_id, + })), + }; +} diff --git a/frontend/appflowy_web_app/src/application/services/services.type.ts b/frontend/appflowy_web_app/src/application/services/services.type.ts index d7d3ad069c..7e170b683b 100644 --- a/frontend/appflowy_web_app/src/application/services/services.type.ts +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -1,5 +1,6 @@ import { YDoc } from '@/application/collab.type'; import { ProviderType, SignUpWithEmailPasswordParams, UserProfile } from '@/application/user.type'; +import * as Y from 'yjs'; export interface AFService { getDeviceID: () => string; @@ -8,6 +9,7 @@ export interface AFService { userService: UserService; documentService: DocumentService; folderService: FolderService; + databaseService: DatabaseService; } export interface AFServiceConfig { @@ -32,6 +34,23 @@ export interface DocumentService { openDocument: (workspaceId: string, docId: string) => Promise; } +export interface DatabaseService { + openDatabase: ( + workspaceId: string, + viewId: string + ) => Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }>; + getDatabase: ( + workspaceId: string, + databaseId: string + ) => Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }>; +} + export interface UserService { getUserProfile: () => Promise; checkUser: () => Promise; diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts new file mode 100644 index 0000000000..8644914ca7 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts @@ -0,0 +1,29 @@ +import { YDoc } from '@/application/collab.type'; +import { DatabaseService } from '@/application/services/services.type'; +import * as Y from 'yjs'; + +export class TauriDatabaseService implements DatabaseService { + constructor() { + // + } + + async openDatabase( + _workspaceId: string, + _viewId: string + ): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + return Promise.reject('Not implemented'); + } + + async getDatabase( + _workspaceId: string, + _databaseId: string + ): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + return Promise.reject('Not implemented'); + } +} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts index 8bcede6523..9ae2987350 100644 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts @@ -1,5 +1,5 @@ import { DocumentService } from '@/application/services/services.type'; -import Y from 'yjs'; +import * as Y from 'yjs'; export class TauriDocumentService implements DocumentService { async openDocument(_id: string): Promise { diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts index 0f162ba36f..8908c002ee 100644 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts @@ -2,11 +2,13 @@ import { AFService, AFServiceConfig, AuthService, + DatabaseService, DocumentService, FolderService, UserService, } from '@/application/services/services.type'; import { TauriAuthService } from '@/application/services/tauri-services/auth.service'; +import { TauriDatabaseService } from '@/application/services/tauri-services/database.service'; import { TauriFolderService } from '@/application/services/tauri-services/folder.service'; import { TauriUserService } from '@/application/services/tauri-services/user.service'; import { TauriDocumentService } from '@/application/services/tauri-services/document.service'; @@ -21,6 +23,8 @@ export class AFClientService implements AFService { folderService: FolderService; + databaseService: DatabaseService; + private deviceId: string = nanoid(8); private clientId: string = 'web'; @@ -41,5 +45,6 @@ export class AFClientService implements AFService { this.userService = new TauriUserService(); this.documentService = new TauriDocumentService(); this.folderService = new TauriFolderService(); + this.databaseService = new TauriDatabaseService(); } } diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts index 1484813ab1..efa2044622 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts @@ -54,7 +54,11 @@ export const YjsEditor = { }, }; -export function withYjs(editor: T, doc: Y.Doc): T & YjsEditor { +export function withYjs( + editor: T, + doc: Y.Doc, + localOrigin: CollabOrigin = CollabOrigin.Local +): T & YjsEditor { const e = editor as T & YjsEditor; const { apply, onChange } = e; @@ -73,11 +77,9 @@ export function withYjs(editor: T, doc: Y.Doc): T & YjsEditor }; const handleYEvents = (events: Array>, transaction: Transaction) => { - if (transaction.origin === CollabOrigin.Local) { - return; + if (transaction.origin === CollabOrigin.Remote) { + YjsEditor.applyRemoteEvents(e, events, transaction); } - - YjsEditor.applyRemoteEvents(e, events, transaction); }; e.connect = () => { @@ -123,7 +125,7 @@ export function withYjs(editor: T, doc: Y.Doc): T & YjsEditor changes.forEach((change) => { applySlateOp(doc, { children: change.slateContent }, change.op); }); - }, CollabOrigin.Local); + }, localOrigin); }; e.apply = (op) => { diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts index edb14cfa0a..5a2fd6670c 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts @@ -1,6 +1,6 @@ import { Operation, Node } from 'slate'; -import Y from 'yjs'; +import * as Y from 'yjs'; -export function applySlateOp (ydoc: Y.Doc, slateRoot: Node, op: Operation) { +export function applySlateOp(ydoc: Y.Doc, slateRoot: Node, op: Operation) { console.log('applySlateOp', op); -} \ No newline at end of file +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts index 3dce8a3d59..dfe5c029e9 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts @@ -1,4 +1,4 @@ -import { YSharedRoot } from '@/application/document.type'; +import { YSharedRoot } from '@/application/collab.type'; import * as Y from 'yjs'; import { Editor, Operation } from 'slate'; diff --git a/frontend/appflowy_web_app/src/application/user.type.ts b/frontend/appflowy_web_app/src/application/user.type.ts index be64d574b4..e2c3bcdb43 100644 --- a/frontend/appflowy_web_app/src/application/user.type.ts +++ b/frontend/appflowy_web_app/src/application/user.type.ts @@ -18,6 +18,11 @@ export interface UserProfile { workspaceId?: string; } +export interface UserWorkspace { + visitingWorkspaceId: string; + workspaces: Workspace[]; +} + export interface Workspace { id: string; name: string; @@ -26,6 +31,8 @@ export interface Workspace { id: number; name: string; }; + type: number; + workspaceDatabaseId: string; } export interface SignUpWithEmailPasswordParams { diff --git a/frontend/appflowy_web_app/src/assets/add.svg b/frontend/appflowy_web_app/src/assets/add.svg deleted file mode 100644 index 049be05cec..0000000000 --- a/frontend/appflowy_web_app/src/assets/add.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/align-center.svg b/frontend/appflowy_web_app/src/assets/align-center.svg deleted file mode 100644 index f4f4999514..0000000000 --- a/frontend/appflowy_web_app/src/assets/align-center.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/align-left.svg b/frontend/appflowy_web_app/src/assets/align-left.svg deleted file mode 100644 index 23957285c7..0000000000 --- a/frontend/appflowy_web_app/src/assets/align-left.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/align-right.svg b/frontend/appflowy_web_app/src/assets/align-right.svg deleted file mode 100644 index bca2d14fc7..0000000000 --- a/frontend/appflowy_web_app/src/assets/align-right.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/arrow-left.svg b/frontend/appflowy_web_app/src/assets/arrow-left.svg deleted file mode 100644 index e4ab9068be..0000000000 --- a/frontend/appflowy_web_app/src/assets/arrow-left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/arrow-right.svg b/frontend/appflowy_web_app/src/assets/arrow-right.svg deleted file mode 100644 index dc40ae52a6..0000000000 --- a/frontend/appflowy_web_app/src/assets/arrow-right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/board.svg b/frontend/appflowy_web_app/src/assets/board.svg deleted file mode 100644 index 0bb0e3fabe..0000000000 --- a/frontend/appflowy_web_app/src/assets/board.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/bold.svg b/frontend/appflowy_web_app/src/assets/bold.svg deleted file mode 100644 index 878b6329b3..0000000000 --- a/frontend/appflowy_web_app/src/assets/bold.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/clock_alarm.svg b/frontend/appflowy_web_app/src/assets/clock_alarm.svg deleted file mode 100644 index 33a5585ceb..0000000000 --- a/frontend/appflowy_web_app/src/assets/clock_alarm.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/close.svg b/frontend/appflowy_web_app/src/assets/close.svg deleted file mode 100644 index b519b419c0..0000000000 --- a/frontend/appflowy_web_app/src/assets/close.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/copy.svg b/frontend/appflowy_web_app/src/assets/copy.svg deleted file mode 100644 index e21e6cb082..0000000000 --- a/frontend/appflowy_web_app/src/assets/copy.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/dark-logo.svg b/frontend/appflowy_web_app/src/assets/dark-logo.svg deleted file mode 100644 index 80d8c4132e..0000000000 --- a/frontend/appflowy_web_app/src/assets/dark-logo.svg +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/database/checkbox-check.svg b/frontend/appflowy_web_app/src/assets/database/checkbox-check.svg deleted file mode 100644 index d2fc54c4b7..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/checkbox-check.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg b/frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg deleted file mode 100644 index 3b3e17dd31..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-attach.svg b/frontend/appflowy_web_app/src/assets/database/field-type-attach.svg deleted file mode 100644 index f00f5c7aa2..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-attach.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg b/frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg deleted file mode 100644 index 37f52c47ed..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg b/frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg deleted file mode 100644 index 3a88d236a1..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-date.svg b/frontend/appflowy_web_app/src/assets/database/field-type-date.svg deleted file mode 100644 index 78243f1e75..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-date.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg b/frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg deleted file mode 100644 index 634af3e361..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg b/frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg deleted file mode 100644 index 97a2e9c434..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-number.svg b/frontend/appflowy_web_app/src/assets/database/field-type-number.svg deleted file mode 100644 index 9d8b98d10d..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-number.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-person.svg b/frontend/appflowy_web_app/src/assets/database/field-type-person.svg deleted file mode 100644 index 2fc04be065..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-person.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-relation.svg b/frontend/appflowy_web_app/src/assets/database/field-type-relation.svg deleted file mode 100644 index f82a41d226..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-relation.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg b/frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg deleted file mode 100644 index 8ccbc9a2e3..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-text.svg b/frontend/appflowy_web_app/src/assets/database/field-type-text.svg deleted file mode 100644 index 7befa5080f..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-text.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-url.svg b/frontend/appflowy_web_app/src/assets/database/field-type-url.svg deleted file mode 100644 index f00f5c7aa2..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-url.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/date.svg b/frontend/appflowy_web_app/src/assets/date.svg deleted file mode 100644 index 78243f1e75..0000000000 --- a/frontend/appflowy_web_app/src/assets/date.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/delete.svg b/frontend/appflowy_web_app/src/assets/delete.svg deleted file mode 100644 index 9e51636798..0000000000 --- a/frontend/appflowy_web_app/src/assets/delete.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/details.svg b/frontend/appflowy_web_app/src/assets/details.svg deleted file mode 100644 index 22c6830916..0000000000 --- a/frontend/appflowy_web_app/src/assets/details.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/document.svg b/frontend/appflowy_web_app/src/assets/document.svg deleted file mode 100644 index b00e1cfb38..0000000000 --- a/frontend/appflowy_web_app/src/assets/document.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/drag.svg b/frontend/appflowy_web_app/src/assets/drag.svg deleted file mode 100644 index 627c959f9f..0000000000 --- a/frontend/appflowy_web_app/src/assets/drag.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/dropdown.svg b/frontend/appflowy_web_app/src/assets/dropdown.svg deleted file mode 100644 index 95e4964b53..0000000000 --- a/frontend/appflowy_web_app/src/assets/dropdown.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/edit.svg b/frontend/appflowy_web_app/src/assets/edit.svg deleted file mode 100644 index ae93287114..0000000000 --- a/frontend/appflowy_web_app/src/assets/edit.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/eye_close.svg b/frontend/appflowy_web_app/src/assets/eye_close.svg deleted file mode 100644 index 116c715ca8..0000000000 --- a/frontend/appflowy_web_app/src/assets/eye_close.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/eye_open.svg b/frontend/appflowy_web_app/src/assets/eye_open.svg deleted file mode 100644 index fa3017c04d..0000000000 --- a/frontend/appflowy_web_app/src/assets/eye_open.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/grid.svg b/frontend/appflowy_web_app/src/assets/grid.svg deleted file mode 100644 index c397af8130..0000000000 --- a/frontend/appflowy_web_app/src/assets/grid.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/h1.svg b/frontend/appflowy_web_app/src/assets/h1.svg deleted file mode 100644 index b33bd52135..0000000000 --- a/frontend/appflowy_web_app/src/assets/h1.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/h2.svg b/frontend/appflowy_web_app/src/assets/h2.svg deleted file mode 100644 index 7449c57391..0000000000 --- a/frontend/appflowy_web_app/src/assets/h2.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/h3.svg b/frontend/appflowy_web_app/src/assets/h3.svg deleted file mode 100644 index 0976945974..0000000000 --- a/frontend/appflowy_web_app/src/assets/h3.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/hide-menu.svg b/frontend/appflowy_web_app/src/assets/hide-menu.svg deleted file mode 100644 index ce88af8ea7..0000000000 --- a/frontend/appflowy_web_app/src/assets/hide-menu.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/hide.svg b/frontend/appflowy_web_app/src/assets/hide.svg deleted file mode 100644 index 22001ef65d..0000000000 --- a/frontend/appflowy_web_app/src/assets/hide.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/image.svg b/frontend/appflowy_web_app/src/assets/image.svg deleted file mode 100644 index 0739605066..0000000000 --- a/frontend/appflowy_web_app/src/assets/image.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/images/default_cover.jpg b/frontend/appflowy_web_app/src/assets/images/default_cover.jpg deleted file mode 100644 index aeaa6a0f29b2dd9639999e5ad43f42c39420caad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 281498 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 z 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

1rhuBp5382z!v%`%^a0uDfTQJghm9_c3~Z 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 - - - diff --git a/frontend/appflowy_web_app/src/assets/italic.svg b/frontend/appflowy_web_app/src/assets/italic.svg deleted file mode 100644 index b295c230f0..0000000000 --- a/frontend/appflowy_web_app/src/assets/italic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/left.svg b/frontend/appflowy_web_app/src/assets/left.svg deleted file mode 100644 index 0f771a3858..0000000000 --- a/frontend/appflowy_web_app/src/assets/left.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/light-logo.svg b/frontend/appflowy_web_app/src/assets/light-logo.svg deleted file mode 100644 index f5cd761ba7..0000000000 --- a/frontend/appflowy_web_app/src/assets/light-logo.svg +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/link.svg b/frontend/appflowy_web_app/src/assets/link.svg deleted file mode 100644 index 5fbcc8d787..0000000000 --- a/frontend/appflowy_web_app/src/assets/link.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/list-dropdown.svg b/frontend/appflowy_web_app/src/assets/list-dropdown.svg deleted file mode 100644 index 4a8424c5f8..0000000000 --- a/frontend/appflowy_web_app/src/assets/list-dropdown.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/list.svg b/frontend/appflowy_web_app/src/assets/list.svg deleted file mode 100644 index 97a2e9c434..0000000000 --- a/frontend/appflowy_web_app/src/assets/list.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/mention.svg b/frontend/appflowy_web_app/src/assets/mention.svg deleted file mode 100644 index b98318132c..0000000000 --- a/frontend/appflowy_web_app/src/assets/mention.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/more.svg b/frontend/appflowy_web_app/src/assets/more.svg deleted file mode 100644 index b191e64a10..0000000000 --- a/frontend/appflowy_web_app/src/assets/more.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/numbers.svg b/frontend/appflowy_web_app/src/assets/numbers.svg deleted file mode 100644 index 9d8b98d10d..0000000000 --- a/frontend/appflowy_web_app/src/assets/numbers.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/open.svg b/frontend/appflowy_web_app/src/assets/open.svg deleted file mode 100644 index b443c8b993..0000000000 --- a/frontend/appflowy_web_app/src/assets/open.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/quote.svg b/frontend/appflowy_web_app/src/assets/quote.svg deleted file mode 100644 index 57839231ff..0000000000 --- a/frontend/appflowy_web_app/src/assets/quote.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/react.svg b/frontend/appflowy_web_app/src/assets/react.svg deleted file mode 100644 index 6c87de9bb3..0000000000 --- a/frontend/appflowy_web_app/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/right.svg b/frontend/appflowy_web_app/src/assets/right.svg deleted file mode 100644 index 7d738f4e69..0000000000 --- a/frontend/appflowy_web_app/src/assets/right.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/search.svg b/frontend/appflowy_web_app/src/assets/search.svg deleted file mode 100644 index a8a92df509..0000000000 --- a/frontend/appflowy_web_app/src/assets/search.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/select-check.svg b/frontend/appflowy_web_app/src/assets/select-check.svg deleted file mode 100644 index 05caec861a..0000000000 --- a/frontend/appflowy_web_app/src/assets/select-check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/settings.svg b/frontend/appflowy_web_app/src/assets/settings.svg deleted file mode 100644 index 92140a3c23..0000000000 --- a/frontend/appflowy_web_app/src/assets/settings.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/settings/account.svg b/frontend/appflowy_web_app/src/assets/settings/account.svg deleted file mode 100644 index fddfca7575..0000000000 --- a/frontend/appflowy_web_app/src/assets/settings/account.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/settings/check_circle.svg b/frontend/appflowy_web_app/src/assets/settings/check_circle.svg deleted file mode 100644 index c6fa56067b..0000000000 --- a/frontend/appflowy_web_app/src/assets/settings/check_circle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/settings/dark.png b/frontend/appflowy_web_app/src/assets/settings/dark.png deleted file mode 100644 index 15a2db5eb8d0b0bfb2fb3e22821eb56f3a8f709e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16280 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 - - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/show-menu.svg b/frontend/appflowy_web_app/src/assets/show-menu.svg deleted file mode 100644 index 8baf55bffd..0000000000 --- a/frontend/appflowy_web_app/src/assets/show-menu.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/sort.svg b/frontend/appflowy_web_app/src/assets/sort.svg deleted file mode 100644 index e3b6a49a56..0000000000 --- a/frontend/appflowy_web_app/src/assets/sort.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/strikethrough.svg b/frontend/appflowy_web_app/src/assets/strikethrough.svg deleted file mode 100644 index c118422a15..0000000000 --- a/frontend/appflowy_web_app/src/assets/strikethrough.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/text.svg b/frontend/appflowy_web_app/src/assets/text.svg deleted file mode 100644 index 7befa5080f..0000000000 --- a/frontend/appflowy_web_app/src/assets/text.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/todo-list.svg b/frontend/appflowy_web_app/src/assets/todo-list.svg deleted file mode 100644 index 37f52c47ed..0000000000 --- a/frontend/appflowy_web_app/src/assets/todo-list.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/underline.svg b/frontend/appflowy_web_app/src/assets/underline.svg deleted file mode 100644 index f5d53f0ec2..0000000000 --- a/frontend/appflowy_web_app/src/assets/underline.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/up.svg b/frontend/appflowy_web_app/src/assets/up.svg deleted file mode 100644 index bd8f3067d3..0000000000 --- a/frontend/appflowy_web_app/src/assets/up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx b/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx index 00441e5281..9216a92c69 100644 --- a/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx @@ -10,7 +10,7 @@ export function RecordNotFound({ open, workspaceId }: { workspaceId: string; ope Oops.. something went wrong - Sorry, the document you are looking for does not exist. + Sorry, the page you are looking for does not exist. diff --git a/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx b/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx index 4fec272b79..be0dc61dc7 100644 --- a/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx @@ -1,10 +1,10 @@ import { ViewLayout, YjsFolderKey, YView } from '@/application/collab.type'; import { useViewSelector } from '@/application/folder-yjs'; import React, { useMemo } from 'react'; -import { ReactComponent as DocumentSvg } from '@/assets/document.svg'; -import { ReactComponent as GridSvg } from '@/assets/grid.svg'; -import { ReactComponent as BoardSvg } from '@/assets/board.svg'; -import { ReactComponent as CalendarSvg } from '@/assets/date.svg'; +import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg'; +import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; +import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; +import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; import { useTranslation } from 'react-i18next'; export function usePageInfo(id: string) { diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx b/frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx new file mode 100644 index 0000000000..f91ac8284e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Popover as PopoverComponent, PopoverProps as PopoverComponentProps } from '@mui/material'; + +const defaultProps: Partial = { + keepMounted: false, + disableRestoreFocus: true, + anchorOrigin: { + vertical: 'bottom', + horizontal: 'left', + }, +}; + +export function Popover({ children, ...props }: PopoverComponentProps) { + return ( + + {children} + + ); +} diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/index.ts b/frontend/appflowy_web_app/src/components/_shared/popover/index.ts new file mode 100644 index 0000000000..8f473de4b9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/popover/index.ts @@ -0,0 +1 @@ +export * from './Popover'; diff --git a/frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx b/frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx new file mode 100644 index 0000000000..f12cfe4c01 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx @@ -0,0 +1,47 @@ +import React, { useMemo } from 'react'; + +function LinearProgressWithLabel({ + value, + count, + selectedCount, +}: { + value: number; + count: number; + selectedCount: number; +}) { + const result = useMemo(() => `${Math.round(value * 100)}%`, [value]); + + const options = useMemo(() => { + return Array.from({ length: count }, (_, i) => ({ + id: i, + checked: i < selectedCount, + })); + }, [count, selectedCount]); + + const isSplit = count < 6; + + return ( +
+
+ {options.map((option) => ( + + ))} +
+
{result}
+
+ ); +} + +export default LinearProgressWithLabel; diff --git a/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx b/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx index 0527b6cc26..9d07c8b908 100644 --- a/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx @@ -7,49 +7,67 @@ export interface AFScrollerProps { overflowYHidden?: boolean; className?: string; style?: React.CSSProperties; + onScroll?: (e: React.UIEvent) => void; } -export const AFScroller = ({ style, children, overflowXHidden, overflowYHidden, className }: AFScrollerProps) => { - return ( -
} - renderThumbVertical={(props) =>
} - {...(overflowXHidden && { - renderTrackHorizontal: (props) => ( + +export const AFScroller = React.forwardRef( + ({ onScroll, style, children, overflowXHidden, overflowYHidden, className }: AFScrollerProps, ref) => { + return ( + { + if (!el) return; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const scrollEl = el.container?.firstChild as HTMLElement; + + if (!scrollEl) return; + if (typeof ref === 'function') { + ref(scrollEl); + } else if (ref) { + ref.current = scrollEl; + } + }} + renderThumbHorizontal={(props) =>
} + renderThumbVertical={(props) =>
} + {...(overflowXHidden && { + renderTrackHorizontal: (props) => ( +
+ ), + })} + {...(overflowYHidden && { + renderTrackVertical: (props) => ( +
+ ), + })} + style={style} + renderView={(props) => (
- ), - })} - {...(overflowYHidden && { - renderTrackVertical: (props) => ( -
- ), - })} - style={style} - renderView={(props) => ( -
- )} - > - {children} - - ); -}; + )} + > + {children} + + ); + } +); diff --git a/frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx b/frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx new file mode 100644 index 0000000000..fbd9ac486d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx @@ -0,0 +1,29 @@ +import { FC, useMemo } from 'react'; + +export interface TagProps { + color?: string; + label?: string; + size?: 'small' | 'medium'; +} + +export const Tag: FC = ({ color, size = 'small', label }) => { + const className = useMemo(() => { + const classList = ['rounded-md', 'font-medium', 'text-xs', 'leading-[18px]']; + + if (color) classList.push(`text-text-title`); + if (size === 'small') classList.push('text-xs', 'px-2', 'py-[2px]'); + if (size === 'medium') classList.push('text-sm', 'px-3', 'py-1'); + return classList.join(' '); + }, [color, size]); + + return ( +
+ {label} +
+ ); +}; diff --git a/frontend/appflowy_web_app/src/components/_shared/tag/index.ts b/frontend/appflowy_web_app/src/components/_shared/tag/index.ts new file mode 100644 index 0000000000..9790fcbf11 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/tag/index.ts @@ -0,0 +1 @@ +export * from './Tag'; diff --git a/frontend/appflowy_web_app/src/components/app/App.tsx b/frontend/appflowy_web_app/src/components/app/App.tsx index 1504c99f07..b2ee81eb20 100644 --- a/frontend/appflowy_web_app/src/components/app/App.tsx +++ b/frontend/appflowy_web_app/src/components/app/App.tsx @@ -10,7 +10,7 @@ const AppMain = withAppWrapper(() => { }> } /> - } /> + } /> } /> diff --git a/frontend/appflowy_web_app/src/components/app/AppTheme.tsx b/frontend/appflowy_web_app/src/components/app/AppTheme.tsx index 2d00bec2a3..179b371125 100644 --- a/frontend/appflowy_web_app/src/components/app/AppTheme.tsx +++ b/frontend/appflowy_web_app/src/components/app/AppTheme.tsx @@ -72,6 +72,7 @@ function AppTheme({ children }: { children: React.ReactNode }) { styleOverrides: { root: { backgroundImage: 'none', + boxShadow: 'var(--shadow)', }, }, }, @@ -100,6 +101,14 @@ function AppTheme({ children }: { children: React.ReactNode }) { }, }, MuiInputBase: { + defaultProps: { + sx: { + '&.Mui-disabled, .Mui-disabled': { + color: 'var(--text-caption)', + WebkitTextFillColor: 'var(--text-caption) !important', + }, + }, + }, styleOverrides: { input: { backgroundColor: 'transparent !important', diff --git a/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx b/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx index f0f83d366a..768cf3587b 100644 --- a/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx +++ b/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx @@ -9,7 +9,7 @@ describe('', () => { it('renders', () => { const AppWrapper = withAppWrapper(Welcome); - + cy.mount(); }); @@ -29,6 +29,7 @@ describe('', () => { cy.wait('@loginSuccess'); cy.wait('@verifyToken'); cy.wait('@getUserProfile'); + cy.wait('@getUserWorkspace'); cy.get('@dialog').should('not.exist'); }); }); diff --git a/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts b/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts index cb972283bf..affe339c81 100644 --- a/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts +++ b/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts @@ -56,6 +56,7 @@ export const useAuth = () => { throw new Error('Failed to check user'); } + console.log('userProfile', userProfile); await setUser(userProfile); return userProfile; diff --git a/frontend/appflowy_web_app/src/components/database/Database.tsx b/frontend/appflowy_web_app/src/components/database/Database.tsx new file mode 100644 index 0000000000..9e54b68ad0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/Database.tsx @@ -0,0 +1,148 @@ +import { DatabaseViewLayout, YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import RecordNotFound from '@/components/_shared/not-found/RecordNotFound'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { Board } from '@/components/database/board'; +import { Calendar } from '@/components/database/calendar'; +import { DatabaseConditionsContext } from '@/components/database/components/conditions/context'; +import { Grid } from '@/components/database/grid'; +import { DatabaseTabs, TabPanel } from '@/components/database/components/tabs'; +import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; +import DatabaseTitle from '@/components/database/DatabaseTitle'; +import { Log } from '@/utils/log'; +import CircularProgress from '@mui/material/CircularProgress'; +import React, { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import SwipeableViews from 'react-swipeable-views'; +import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions'; +import * as Y from 'yjs'; + +export const Database = memo(() => { + const { objectId, workspaceId } = useId() || {}; + const [search, setSearch] = useSearchParams(); + const viewId = search.get('v'); + + const [doc, setDoc] = useState(null); + const [rows, setRows] = useState | null>(null); // Map(false); + const databaseService = useContext(AFConfigContext)?.service?.databaseService; + + const handleOpenDocument = useCallback(async () => { + if (!databaseService || !workspaceId || !objectId) return; + + try { + setDoc(null); + const { databaseDoc, rows } = await databaseService.openDatabase(workspaceId, objectId); + + console.log('databaseDoc', databaseDoc.getMap(YjsEditorKey.data_section).toJSON()); + setDoc(databaseDoc); + setRows(rows); + } catch (e) { + Log.error(e); + setNotFound(true); + } + }, [databaseService, workspaceId, objectId]); + + useEffect(() => { + setNotFound(false); + void handleOpenDocument(); + }, [handleOpenDocument]); + + const database = useMemo(() => doc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase, [doc]); + + const views = useMemo(() => database?.get(YjsDatabaseKey.views), [database]); + + const handleChangeView = useCallback( + (viewId: string) => { + setSearch({ v: viewId }); + }, + [setSearch] + ); + + const viewIds = useMemo(() => (views ? Array.from(views.keys()) : []), [views]); + + const value = useMemo(() => { + return Math.max( + 0, + viewIds.findIndex((id) => id === (viewId ?? objectId)) + ); + }, [viewId, viewIds, objectId]); + + const getDatabaseViewComponent = useCallback((layout: DatabaseViewLayout) => { + switch (layout) { + case DatabaseViewLayout.Grid: + return Grid; + case DatabaseViewLayout.Board: + return Board; + case DatabaseViewLayout.Calendar: + return Calendar; + } + }, []); + + const [conditionsExpanded, setConditionsExpanded] = useState(false); + const toggleExpanded = useCallback(() => { + setConditionsExpanded((prev) => !prev); + }, []); + + console.log('viewId', viewId, 'objectId', doc, objectId, database); + if (!objectId) return null; + + if (!doc) { + return ; + } + + if (!rows) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ + + + + + + {viewIds.map((viewId, index) => { + const layout = Number(views.get(viewId)?.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; + const Component = getDatabaseViewComponent(layout); + + return ( + + + + ); + })} + + +
+
+ ); +}); + +export default Database; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx new file mode 100644 index 0000000000..8adc87d4e6 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx @@ -0,0 +1,10 @@ +import { DatabaseContext, DatabaseContextState } from '@/application/database-yjs'; + +export const DatabaseContextProvider = ({ + children, + ...props +}: DatabaseContextState & { + children: React.ReactNode; +}) => { + return {children}; +}; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx new file mode 100644 index 0000000000..baf314130e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx @@ -0,0 +1,19 @@ +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; +import React from 'react'; + +function DatabaseTitle({ viewId }: { viewId: string }) { + const { name, icon } = usePageInfo(viewId); + + return ( +
+
+
+
{icon}
+
{name}
+
+
+
+ ); +} + +export default DatabaseTitle; diff --git a/frontend/appflowy_web_app/src/components/database/board/Board.tsx b/frontend/appflowy_web_app/src/components/database/board/Board.tsx new file mode 100644 index 0000000000..eabc9c2631 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/board/Board.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export function Board() { + return
Board
; +} + +export default Board; diff --git a/frontend/appflowy_web_app/src/components/database/board/index.ts b/frontend/appflowy_web_app/src/components/database/board/index.ts new file mode 100644 index 0000000000..9294d869ce --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/board/index.ts @@ -0,0 +1 @@ +export * from './Board'; diff --git a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx new file mode 100644 index 0000000000..c21e37b362 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export function Calendar() { + return
Calendar
; +} + +export default Calendar; diff --git a/frontend/appflowy_web_app/src/components/database/calendar/index.ts b/frontend/appflowy_web_app/src/components/database/calendar/index.ts new file mode 100644 index 0000000000..a723380592 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/index.ts @@ -0,0 +1 @@ +export * from './Calendar'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx new file mode 100644 index 0000000000..eeefee18bb --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx @@ -0,0 +1,40 @@ +import { CalculationType } from '@/application/database-yjs/database.type'; +import { CalulationCell } from '@/components/database/components/calculation-cell/cell.type'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function CalculationCell({ cell }: { cell?: CalulationCell }) { + const { t } = useTranslation(); + + const prefix = useMemo(() => { + if (!cell) return ''; + + switch (cell.type) { + case CalculationType.Average: + return t('grid.calculationTypeLabel.average'); + case CalculationType.Max: + return t('grid.calculationTypeLabel.max'); + case CalculationType.Count: + return t('grid.calculationTypeLabel.count'); + case CalculationType.Min: + return t('grid.calculationTypeLabel.min'); + case CalculationType.Sum: + return t('grid.calculationTypeLabel.sum'); + case CalculationType.CountEmpty: + return t('grid.calculationTypeLabel.countEmptyShort'); + case CalculationType.CountNonEmpty: + return t('grid.calculationTypeLabel.countNonEmptyShort'); + default: + return ''; + } + }, [cell, t]); + + return ( +
+ {prefix} + {cell?.value ?? ''} +
+ ); +} + +export default CalculationCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts new file mode 100644 index 0000000000..ef44e2e745 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts @@ -0,0 +1,8 @@ +import { CalculationType } from '@/application/database-yjs/database.type'; + +export interface CalulationCell { + value: string; + fieldId: string; + id: string; + type: CalculationType; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts new file mode 100644 index 0000000000..9bf73af548 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts @@ -0,0 +1 @@ +export * from './CalculationCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts new file mode 100644 index 0000000000..1012dd4543 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts @@ -0,0 +1,47 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { useFieldSelector } from '@/application/database-yjs/selector'; +import { DateFormat, TimeFormat, getDateFormat, getTimeFormat } from '@/application/database-yjs'; +import { renderDate } from '@/utils/time'; +import { useCallback, useMemo } from 'react'; + +export function useCellTypeOption(fieldId: string) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + return useMemo(() => { + return field?.get(YjsDatabaseKey.type_option)?.get(String(fieldType)); + }, [fieldType, field]); +} + +export function useDateTypeCellDispatcher(fieldId: string) { + const typeOption = useCellTypeOption(fieldId); + const typeOptionValue = useMemo(() => { + if (!typeOption) return null; + return { + timeFormat: parseInt(typeOption.get(YjsDatabaseKey.time_format)) as TimeFormat, + dateFormat: parseInt(typeOption.get(YjsDatabaseKey.date_format)) as DateFormat, + }; + }, [typeOption]); + + const getDateTimeStr = useCallback( + (timeStamp: string, includeTime?: boolean) => { + if (!typeOptionValue) return null; + const timeFormat = getTimeFormat(typeOptionValue.timeFormat); + const dateFormat = getDateFormat(typeOptionValue.dateFormat); + const format = [dateFormat]; + + if (includeTime) { + format.push(timeFormat); + } + + return renderDate(timeStamp, format.join(' '), true); + }, + [typeOptionValue] + ); + + return { + getDateTimeStr, + typeOptionValue, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx new file mode 100644 index 0000000000..ee3cde673b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx @@ -0,0 +1,62 @@ +import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { useFieldSelector } from '@/application/database-yjs/selector'; +import RowCreateModifiedTime from '@/components/database/components/cell/RowCreateModifiedTime'; +import React, { FC, useMemo } from 'react'; +import RichTextCell from '@/components/database/components/cell/TextCell'; +import UrlCell from '@/components/database/components/cell/UrlCell'; +import NumberCell from '@/components/database/components/cell/NumberCell'; +import CheckboxCell from '@/components/database/components/cell/CheckboxCell'; +import SelectCell from '@/components/database/components/cell/SelectionCell'; +import DateTimeCell from '@/components/database/components/cell/DateTimeCell'; +import ChecklistCell from '@/components/database/components/cell/ChecklistCell'; +import { Cell as CellValue } from '@/components/database/components/cell/cell.type'; +import RelationCell from '@/components/database/components/cell/RelationCell'; + +export interface CellProps { + rowId: string; + fieldId: FieldId; + cell?: CellValue; +} + +export function Cell({ cell, rowId, fieldId }: CellProps) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + const Component = useMemo(() => { + switch (fieldType) { + case FieldType.RichText: + return RichTextCell; + case FieldType.URL: + return UrlCell; + case FieldType.Number: + return NumberCell; + case FieldType.Checkbox: + return CheckboxCell; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return SelectCell; + case FieldType.DateTime: + return DateTimeCell; + case FieldType.Checklist: + return ChecklistCell; + case FieldType.Relation: + return RelationCell; + default: + return RichTextCell; + } + }, [fieldType]) as FC<{ cell?: CellValue; rowId: string; fieldId: FieldId }>; + + if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) { + const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified; + + return ; + } + + if (cell?.fieldType !== fieldType) { + return null; + } + + return ; +} + +export default Cell; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx new file mode 100644 index 0000000000..558c424f62 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx @@ -0,0 +1,14 @@ +import { FieldId } from '@/application/collab.type'; +import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; +import { CheckboxCell } from '@/components/database/components/cell/cell.type'; + +export default function ({ cell }: { cell?: CheckboxCell; rowId: string; fieldId: FieldId }) { + const checked = cell?.data; + + return ( +
+ {checked ? : } +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx new file mode 100644 index 0000000000..32d97d758f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx @@ -0,0 +1,21 @@ +import { FieldId } from '@/application/collab.type'; +import { parseChecklistData } from '@/application/database-yjs'; +import { ChecklistCell } from '@/components/database/components/cell/cell.type'; +import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel'; +import React, { useMemo } from 'react'; + +export default function ({ cell }: { cell?: ChecklistCell; rowId: string; fieldId: FieldId }) { + const data = useMemo(() => { + return parseChecklistData(cell?.data ?? ''); + }, [cell?.data]); + + const options = data?.options; + const selectedOptions = data?.selectedOptionIds; + + if (!data || !options || !selectedOptions) return null; + return ( +
+ +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx new file mode 100644 index 0000000000..490a2bd95e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx @@ -0,0 +1,35 @@ +import { FieldId } from '@/application/collab.type'; +import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; +import { DateTimeCell } from '@/components/database/components/cell/cell.type'; +import React, { useMemo } from 'react'; +import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg'; + +export default function ({ cell, fieldId }: { cell?: DateTimeCell; rowId: string; fieldId: FieldId }) { + const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); + + const startDateTime = useMemo(() => { + return getDateTimeStr(cell?.data || '', cell?.includeTime); + }, [cell, getDateTimeStr]); + + const endDateTime = useMemo(() => { + if (!cell) return null; + const { endTimestamp, isRange } = cell; + + if (!isRange) return null; + + return getDateTimeStr(endTimestamp || '', cell?.includeTime); + }, [cell, getDateTimeStr]); + + const dateStr = useMemo(() => { + return [startDateTime, endDateTime].filter(Boolean).join(' -> '); + }, [startDateTime, endDateTime]); + + const hasReminder = !!cell?.reminderId; + + return ( +
+ {hasReminder && } + {dateStr} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx new file mode 100644 index 0000000000..851e14a34e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx @@ -0,0 +1,27 @@ +import { FieldId } from '@/application/collab.type'; +import { currencyFormaterMap, NumberFormat, useFieldSelector, parseNumberTypeOptions } from '@/application/database-yjs'; +import { UrlCell } from '@/components/database/components/cell/cell.type'; +import React, { useMemo } from 'react'; +import Decimal from 'decimal.js'; + +export default function ({ cell, fieldId }: { cell?: UrlCell; rowId: string; fieldId: FieldId }) { + const { field } = useFieldSelector(fieldId); + + const format = useMemo(() => (field ? parseNumberTypeOptions(field).format : NumberFormat.Num), [field]); + + const className = useMemo(() => { + const classList = ['select-text', 'cursor-text']; + + return classList.join(' '); + }, []); + + const value = useMemo(() => { + if (!cell) return ''; + const numberFormater = currencyFormaterMap[format]; + + if (!numberFormater) return cell.data; + return numberFormater(new Decimal(cell.data).toNumber()); + }, [cell, format]); + + return
{value}
; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx new file mode 100644 index 0000000000..56c1e8d27b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx @@ -0,0 +1,84 @@ +import { + FieldId, + YDatabaseField, + YDatabaseFields, + YDatabaseRow, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; +import { useFieldSelector, parseRelationTypeOption } from '@/application/database-yjs'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse'; +import { RelationCell, RelationCellData } from '@/components/database/components/cell/cell.type'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import * as Y from 'yjs'; + +export default function ({ cell, fieldId }: { cell?: RelationCell; fieldId: string; rowId: string }) { + const { field } = useFieldSelector(fieldId); + const workspaceId = useId()?.workspaceId; + const rowIds = useMemo(() => (cell?.data.toJSON() as RelationCellData) ?? [], [cell?.data]); + const databaseId = rowIds.length > 0 && field ? parseRelationTypeOption(field).database_id : undefined; + const databaseService = useContext(AFConfigContext)?.service?.databaseService; + const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState(undefined); + const [rows, setRows] = useState | null>(); + + useEffect(() => { + if (!workspaceId || !databaseId) return; + void databaseService?.getDatabase(workspaceId, databaseId).then(({ databaseDoc: doc, rows }) => { + const fields = doc + .getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.database) + .get(YjsDatabaseKey.fields) as YDatabaseFields; + + fields.forEach((field, fieldId) => { + if ((field as YDatabaseField).get(YjsDatabaseKey.is_primary)) { + setDatabasePrimaryFieldId(fieldId); + } + }); + + setRows(rows); + }); + }, [workspaceId, databaseId, databaseService]); + + return ( +
+ {rowIds.map((rowId) => { + const rowDoc = rows?.get(rowId); + + return ( +
+ {rowDoc && databasePrimaryFieldId && ( + + )} +
+ ); + })} +
+ ); +} + +function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) { + const [text, setText] = useState(null); + + useEffect(() => { + const row = rowDoc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + const cells = row.get(YjsDatabaseKey.cells); + const primaryCell = cells.get(fieldId); + + if (!primaryCell) return; + const observeHandler = () => { + setText(parseYDatabaseCellToCell(primaryCell).data as string); + }; + + observeHandler(); + + primaryCell.observe(observeHandler); + return () => { + primaryCell.unobserve(observeHandler); + }; + }, [rowDoc, fieldId]); + + return
{text}
; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx new file mode 100644 index 0000000000..d685b53cf9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx @@ -0,0 +1,43 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { useRowMeta } from '@/application/database-yjs'; +import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; +import React, { useEffect, useMemo, useState } from 'react'; + +function RowCreateModifiedTime({ + rowId, + fieldId, + attrName, +}: { + rowId: string; + fieldId: string; + attrName: YjsDatabaseKey.last_modified | YjsDatabaseKey.created_at; +}) { + const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); + const rowMeta = useRowMeta(rowId); + const [value, setValue] = useState(null); + + useEffect(() => { + if (!rowMeta) return; + const observeHandler = () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + setValue(rowMeta.get(attrName)); + }; + + observeHandler(); + + rowMeta.observe(observeHandler); + return () => { + rowMeta.unobserve(observeHandler); + }; + }, [rowMeta, attrName]); + + const time = useMemo(() => { + if (!value) return null; + return getDateTimeStr(value, false); + }, [value, getDateTimeStr]); + + return
{time}
; +} + +export default RowCreateModifiedTime; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx new file mode 100644 index 0000000000..a915d31a9b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx @@ -0,0 +1,32 @@ +import { FieldId } from '@/application/collab.type'; +import { useFieldSelector, parseSelectOptionTypeOptions } from '@/application/database-yjs'; +import { Tag } from '@/components/_shared/tag'; +import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; +import { SelectCell } from '@/components/database/components/cell/cell.type'; +import React, { useCallback, useMemo } from 'react'; + +export default function ({ cell, fieldId }: { cell?: SelectCell; rowId: string; fieldId: FieldId }) { + const selectOptionIds = useMemo(() => cell?.data.split(','), [cell]); + const { field } = useFieldSelector(fieldId); + const typeOption = useMemo(() => { + if (!field) return null; + return parseSelectOptionTypeOptions(field); + }, [field]); + + const renderSelectedOptions = useCallback( + (selected: string[]) => + selected.map((id) => { + const option = typeOption?.options?.find((option) => option.id === id); + + if (!option) return null; + return ; + }), + [typeOption] + ); + + return ( +
+ {selectOptionIds ? renderSelectedOptions(selectOptionIds) : null} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx new file mode 100644 index 0000000000..f9c8749258 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx @@ -0,0 +1,12 @@ +import { FieldId } from '@/application/collab.type'; +import { useReadOnly } from '@/application/database-yjs'; +import { TextCell } from '@/components/database/components/cell/cell.type'; +import React from 'react'; + +function TextCellComponent({ cell }: { cell?: TextCell; rowId: string; fieldId: FieldId }) { + const readOnly = useReadOnly(); + + return
{cell?.data}
; +} + +export default TextCellComponent; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx new file mode 100644 index 0000000000..e2d3d2c87f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx @@ -0,0 +1,37 @@ +import { FieldId } from '@/application/collab.type'; +import { useReadOnly } from '@/application/database-yjs'; +import { UrlCell } from '@/components/database/components/cell/cell.type'; +import { openUrl, processUrl } from '@/utils/url'; +import React, { useMemo } from 'react'; + +export default function ({ cell }: { cell?: UrlCell; rowId: string; fieldId: FieldId }) { + const readOnly = useReadOnly(); + + const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]); + + const className = useMemo(() => { + const classList = ['select-text']; + + if (isUrl) { + classList.push('text-content-blue-400', 'underline', 'cursor-pointer'); + } else { + classList.push('cursor-text'); + } + + return classList.join(' '); + }, [isUrl]); + + return ( +
{ + if (!isUrl || !cell) return; + if (readOnly) { + void openUrl(cell.data, '_blank'); + } + }} + className={className} + > + {cell?.data} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts b/frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts new file mode 100644 index 0000000000..d9e3564096 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts @@ -0,0 +1,25 @@ +import { SelectOptionColor } from '@/application/database-yjs'; + +export const SelectOptionColorMap = { + [SelectOptionColor.Purple]: '--tint-purple', + [SelectOptionColor.Pink]: '--tint-pink', + [SelectOptionColor.LightPink]: '--tint-red', + [SelectOptionColor.Orange]: '--tint-orange', + [SelectOptionColor.Yellow]: '--tint-yellow', + [SelectOptionColor.Lime]: '--tint-lime', + [SelectOptionColor.Green]: '--tint-green', + [SelectOptionColor.Aqua]: '--tint-aqua', + [SelectOptionColor.Blue]: '--tint-blue', +}; + +export const SelectOptionColorTextMap = { + [SelectOptionColor.Purple]: 'purpleColor', + [SelectOptionColor.Pink]: 'pinkColor', + [SelectOptionColor.LightPink]: 'lightPinkColor', + [SelectOptionColor.Orange]: 'orangeColor', + [SelectOptionColor.Yellow]: 'yellowColor', + [SelectOptionColor.Lime]: 'limeColor', + [SelectOptionColor.Green]: 'greenColor', + [SelectOptionColor.Aqua]: 'aquaColor', + [SelectOptionColor.Blue]: 'blueColor', +} as const; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/cell.parse.ts b/frontend/appflowy_web_app/src/components/database/components/cell/cell.parse.ts new file mode 100644 index 0000000000..4124381c06 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/cell.parse.ts @@ -0,0 +1,46 @@ +import { YDatabaseCell, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { Cell, CheckboxCell, DateTimeCell } from './cell.type'; + +export function parseYDatabaseCommonCellToCell(cell: YDatabaseCell): Cell { + return { + createdAt: Number(cell.get(YjsDatabaseKey.created_at)), + lastModified: Number(cell.get(YjsDatabaseKey.last_modified)), + fieldType: parseInt(cell.get(YjsDatabaseKey.field_type)) as FieldType, + data: cell.get(YjsDatabaseKey.data), + }; +} + +export function parseYDatabaseCellToCell(cell: YDatabaseCell): Cell { + const fieldType = parseInt(cell.get(YjsDatabaseKey.field_type)); + + if (fieldType === FieldType.DateTime) { + return parseYDatabaseDateTimeCellToCell(cell); + } + + if (fieldType === FieldType.Checkbox) { + return parseYDatabaseCheckboxCellToCell(cell); + } + + return parseYDatabaseCommonCellToCell(cell); +} + +export function parseYDatabaseDateTimeCellToCell(cell: YDatabaseCell): DateTimeCell { + return { + ...parseYDatabaseCommonCellToCell(cell), + data: cell.get(YjsDatabaseKey.data) as string, + fieldType: FieldType.DateTime, + endTimestamp: cell.get(YjsDatabaseKey.end_timestamp), + includeTime: cell.get(YjsDatabaseKey.include_time), + isRange: cell.get(YjsDatabaseKey.is_range), + reminderId: cell.get(YjsDatabaseKey.reminder_id), + }; +} + +export function parseYDatabaseCheckboxCellToCell(cell: YDatabaseCell): CheckboxCell { + return { + ...parseYDatabaseCommonCellToCell(cell), + data: cell.get(YjsDatabaseKey.data) === 'Yes', + fieldType: FieldType.Checkbox, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts b/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts new file mode 100644 index 0000000000..185cca9409 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts @@ -0,0 +1,90 @@ +import { RowId } from '@/application/collab.type'; +import { DateFormat, SelectOption, TimeFormat } from '@/application/database-yjs'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { YArray } from 'yjs/dist/src/types/YArray'; + +export interface Cell { + createdAt: number; + lastModified: number; + fieldType: FieldType; + data: unknown; +} + +export interface TextCell extends Cell { + fieldType: FieldType.RichText; + data: string; +} + +export interface NumberCell extends Cell { + fieldType: FieldType.Number; + data: string; +} + +export interface CheckboxCell extends Cell { + fieldType: FieldType.Checkbox; + data: boolean; +} + +export interface UrlCell extends Cell { + fieldType: FieldType.URL; + data: string; +} + +export type SelectionId = string; + +export interface SelectCell extends Cell { + fieldType: FieldType.SingleSelect | FieldType.MultiSelect; + data: SelectionId; +} + +export interface DataTimeTypeOption { + timeFormat: TimeFormat; + dateFormat: DateFormat; +} + +export interface DateTimeCell extends Cell { + fieldType: FieldType.DateTime; + data: string; + endTimestamp?: string; + includeTime?: boolean; + isRange?: boolean; + reminderId?: string; +} + +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 { + fieldType: FieldType.Checklist; + data: string; +} + +export interface RelationCell extends Cell { + fieldType: FieldType.Relation; + data: YArray; +} + +export type RelationCellData = RowId[]; + +export interface ChecklistCellData { + selected_option_ids?: string[]; + options?: SelectOption[]; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/index.ts new file mode 100644 index 0000000000..2440976340 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/index.ts @@ -0,0 +1 @@ +export * from './Cell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx new file mode 100644 index 0000000000..6b4a836597 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx @@ -0,0 +1,35 @@ +import { useFiltersSelector, useSortsSelector } from '@/application/database-yjs'; +import { useConditionsContext } from '@/components/database/components/conditions/context'; +import { TextButton } from '@/components/database/components/tabs/TextButton'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export function DatabaseActions() { + const { t } = useTranslation(); + const sorts = useSortsSelector(); + const filter = useFiltersSelector(); + const conditionsContext = useConditionsContext(); + + return ( +
+ { + conditionsContext?.toggleExpanded(); + }} + color={filter.length > 0 ? 'primary' : 'inherit'} + > + {t('grid.settings.filter')} + + { + conditionsContext?.toggleExpanded(); + }} + color={sorts.length > 0 ? 'primary' : 'inherit'} + > + {t('grid.settings.sort')} + +
+ ); +} + +export default DatabaseActions; diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx new file mode 100644 index 0000000000..fc36c470d6 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx @@ -0,0 +1,33 @@ +import { useFiltersSelector, useSortsSelector } from '@/application/database-yjs'; +import { AFScroller } from '@/components/_shared/scroller'; +import { useConditionsContext } from '@/components/database/components/conditions/context'; +import React from 'react'; +import Filters from 'src/components/database/components/filters/Filters'; +import Sorts from 'src/components/database/components/sorts/Sorts'; + +export function DatabaseConditions() { + const conditionsContext = useConditionsContext(); + const expanded = conditionsContext?.expanded ?? false; + const sorts = useSortsSelector(); + const filters = useFiltersSelector(); + + return ( +
+ + + {sorts.length > 0 && filters.length > 0 &&
} + + +
+ ); +} + +export default DatabaseConditions; diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/context.ts b/frontend/appflowy_web_app/src/components/database/components/conditions/context.ts new file mode 100644 index 0000000000..aadb5007af --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/context.ts @@ -0,0 +1,12 @@ +import { createContext, useContext } from 'react'; + +interface DatabaseConditionsContextType { + expanded: boolean; + toggleExpanded: () => void; +} + +export function useConditionsContext() { + return useContext(DatabaseConditionsContext); +} + +export const DatabaseConditionsContext = createContext(undefined); diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/index.ts b/frontend/appflowy_web_app/src/components/database/components/conditions/index.ts new file mode 100644 index 0000000000..7b30286c5c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/index.ts @@ -0,0 +1,2 @@ +export * from './DatabaseActions'; +export * from './DatabaseConditions'; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx b/frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx new file mode 100644 index 0000000000..3ff135e8f7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx @@ -0,0 +1,20 @@ +import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, useFieldSelector } from '@/application/database-yjs'; +import { FieldTypeIcon } from '@/components/database/components/field/FieldTypeIcon'; +import React from 'react'; + +export function FieldDisplay({ fieldId }: { fieldId: FieldId }) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + if (!field) return null; + + return ( +
+ + {field?.get(YjsDatabaseKey.name)} +
+ ); +} + +export default FieldDisplay; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx b/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx new file mode 100644 index 0000000000..3749e21afd --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx @@ -0,0 +1,33 @@ +import { FieldType } from '@/application/database-yjs/database.type'; +import { FC, memo } from 'react'; +import { ReactComponent as TextSvg } from '$icons/16x/text.svg'; +import { ReactComponent as NumberSvg } from '$icons/16x/number.svg'; +import { ReactComponent as DateSvg } from '$icons/16x/date.svg'; +import { ReactComponent as SingleSelectSvg } from '$icons/16x/single_select.svg'; +import { ReactComponent as MultiSelectSvg } from '$icons/16x/multiselect.svg'; +import { ReactComponent as ChecklistSvg } from '$icons/16x/checklist.svg'; +import { ReactComponent as CheckboxSvg } from '$icons/16x/checkbox.svg'; +import { ReactComponent as URLSvg } from '$icons/16x/url.svg'; +import { ReactComponent as LastEditedTimeSvg } from '$icons/16x/last_modified.svg'; +import { ReactComponent as CreatedSvg } from '$icons/16x/created_at.svg'; +import { ReactComponent as RelationSvg } from '$icons/16x/relation.svg'; + +export const FieldTypeSvgMap: Record>> = { + [FieldType.RichText]: TextSvg, + [FieldType.Number]: NumberSvg, + [FieldType.DateTime]: DateSvg, + [FieldType.SingleSelect]: SingleSelectSvg, + [FieldType.MultiSelect]: MultiSelectSvg, + [FieldType.Checkbox]: CheckboxSvg, + [FieldType.URL]: URLSvg, + [FieldType.Checklist]: ChecklistSvg, + [FieldType.LastEditedTime]: LastEditedTimeSvg, + [FieldType.CreatedTime]: CreatedSvg, + [FieldType.Relation]: RelationSvg, +}; + +export const FieldTypeIcon: FC<{ type: FieldType; className?: string }> = memo(({ type, ...props }) => { + const Svg = FieldTypeSvgMap[type]; + + return ; +}); diff --git a/frontend/appflowy_web_app/src/components/database/components/field/index.ts b/frontend/appflowy_web_app/src/components/database/components/field/index.ts new file mode 100644 index 0000000000..85ff96da07 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/index.ts @@ -0,0 +1,2 @@ +export * from './FieldTypeIcon'; +export * from './FieldDisplay'; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx b/frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx new file mode 100644 index 0000000000..353ef5d349 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx @@ -0,0 +1,30 @@ +import { parseSelectOptionTypeOptions, SelectOption, useFieldSelector } from '@/application/database-yjs'; +import { Tag } from '@/components/_shared/tag'; +import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; +import React, { useCallback, useMemo } from 'react'; +import { ReactComponent as CheckIcon } from '$icons/16x/check.svg'; + +export function SelectOptionList({ fieldId, selectedIds }: { fieldId: string; selectedIds: string[] }) { + const { field } = useFieldSelector(fieldId); + const typeOption = useMemo(() => { + if (!field) return null; + return parseSelectOptionTypeOptions(field); + }, [field]); + + const renderOption = useCallback( + (option: SelectOption) => { + const isSelected = selectedIds.includes(option.id); + + return ( +
+ + {isSelected && } +
+ ); + }, + [selectedIds] + ); + + if (!field || !typeOption) return null; + return
{typeOption.options.map(renderOption)}
; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts b/frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts new file mode 100644 index 0000000000..20465070b4 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts @@ -0,0 +1 @@ +export * from './SelectOptionList'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx new file mode 100644 index 0000000000..3fe0c4daf3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx @@ -0,0 +1,57 @@ +import { useFilterSelector } from '@/application/database-yjs'; +import { Popover } from '@/components/_shared/popover'; +import { FilterContentOverview } from './overview'; +import React, { useState } from 'react'; +import { FieldDisplay } from '@/components/database/components/field'; +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; +import { FilterMenu } from './filter-menu'; + +function Filter({ filterId }: { filterId: string }) { + const filter = useFilterSelector(filterId); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + if (!filter) return null; + + return ( + <> +
{ + setAnchorEl(e.currentTarget); + }} + className={ + 'flex cursor-pointer flex-nowrap items-center gap-1 rounded-full border border-line-divider py-1 px-2 hover:border-fill-default hover:text-fill-default hover:shadow-sm' + } + > +
+ +
+ +
+ +
+ +
+ {open && ( + { + setAnchorEl(null); + }} + slotProps={{ + paper: { + style: { + maxHeight: '260px', + }, + }, + }} + > + + + )} + + ); +} + +export default Filter; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx new file mode 100644 index 0000000000..41f54f8cac --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx @@ -0,0 +1,32 @@ +import { useFiltersSelector, useReadOnly } from '@/application/database-yjs'; +import Filter from '@/components/database/components/filters/Filter'; +import Button from '@mui/material/Button'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as AddFilterSvg } from '$icons/16x/add.svg'; + +export function Filters() { + const filters = useFiltersSelector(); + const { t } = useTranslation(); + const readOnly = useReadOnly(); + + return ( + <> + {filters.map((filterId) => ( + + ))} + + + ); +} + +export default Filters; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx new file mode 100644 index 0000000000..851e811499 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx @@ -0,0 +1,33 @@ +import { CheckboxFilter, CheckboxFilterCondition } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function CheckboxFilterMenu({ filter }: { filter: CheckboxFilter }) { + const { t } = useTranslation(); + + const conditions = useMemo( + () => [ + { + value: CheckboxFilterCondition.IsChecked, + text: t('grid.checkboxFilter.isChecked'), + }, + { + value: CheckboxFilterCondition.IsUnChecked, + text: t('grid.checkboxFilter.isUnchecked'), + }, + ], + [t] + ); + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + return ( +
+ +
+ ); +} + +export default CheckboxFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx new file mode 100644 index 0000000000..5d6398b242 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx @@ -0,0 +1,33 @@ +import { ChecklistFilter, ChecklistFilterCondition } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function ChecklistFilterMenu({ filter }: { filter: ChecklistFilter }) { + const { t } = useTranslation(); + + const conditions = useMemo( + () => [ + { + value: ChecklistFilterCondition.IsComplete, + text: t('grid.checklistFilter.isComplete'), + }, + { + value: ChecklistFilterCondition.IsIncomplete, + text: t('grid.checklistFilter.isIncomplted'), + }, + ], + [t] + ); + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + return ( +
+ +
+ ); +} + +export default ChecklistFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx new file mode 100644 index 0000000000..e5784b44f5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx @@ -0,0 +1,23 @@ +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; +import { FieldDisplay } from '@/components/database/components/field'; +import React from 'react'; + +function FieldMenuTitle({ fieldId, selectedConditionText }: { fieldId: string; selectedConditionText: string }) { + return ( +
+
+ +
+
+
+
+ {selectedConditionText} +
+ +
+
+
+ ); +} + +export default FieldMenuTitle; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx new file mode 100644 index 0000000000..720dac3d3d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx @@ -0,0 +1,39 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, Filter, SelectOptionFilter, useFieldSelector } from '@/application/database-yjs'; +import CheckboxFilterMenu from './CheckboxFilterMenu'; +import ChecklistFilterMenu from './ChecklistFilterMenu'; +import MultiSelectOptionFilterMenu from './MultiSelectOptionFilterMenu'; +import NumberFilterMenu from './NumberFilterMenu'; +import SingleSelectOptionFilterMenu from './SingleSelectOptionFilterMenu'; +import TextFilterMenu from './TextFilterMenu'; +import React, { useMemo } from 'react'; + +export function FilterMenu({ filter }: { filter: Filter }) { + const { field } = useFieldSelector(filter?.fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + const menu = useMemo(() => { + if (!field) return null; + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return ; + case FieldType.Checkbox: + return ; + case FieldType.Checklist: + return ; + case FieldType.Number: + return ; + case FieldType.MultiSelect: + return ; + case FieldType.SingleSelect: + return ; + default: + return null; + } + }, [field, fieldType, filter]); + + return menu; +} + +export default FilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx new file mode 100644 index 0000000000..68def09bb8 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx @@ -0,0 +1,56 @@ +import { SelectOptionFilter, SelectOptionFilterCondition } from '@/application/database-yjs'; +import { SelectOptionList } from '@/components/database/components/field/select-option'; +import FieldMenuTitle from './FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function MultiSelectOptionFilterMenu({ filter }: { filter: SelectOptionFilter }) { + const { t } = useTranslation(); + const conditions = useMemo(() => { + return [ + { + value: SelectOptionFilterCondition.OptionIs, + text: t('grid.selectOptionFilter.is'), + }, + { + value: SelectOptionFilterCondition.OptionIsNot, + text: t('grid.selectOptionFilter.isNot'), + }, + { + value: SelectOptionFilterCondition.OptionContains, + text: t('grid.selectOptionFilter.contains'), + }, + { + value: SelectOptionFilterCondition.OptionDoesNotContain, + text: t('grid.selectOptionFilter.doesNotContain'), + }, + { + value: SelectOptionFilterCondition.OptionIsEmpty, + text: t('grid.selectOptionFilter.isEmpty'), + }, + { + value: SelectOptionFilterCondition.OptionIsNotEmpty, + text: t('grid.selectOptionFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displaySelectOptionList = useMemo(() => { + return ![SelectOptionFilterCondition.OptionIsEmpty, SelectOptionFilterCondition.OptionIsNotEmpty].includes( + filter.condition + ); + }, [filter.condition]); + + return ( +
+ + {displaySelectOptionList && } +
+ ); +} + +export default MultiSelectOptionFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx new file mode 100644 index 0000000000..fdd8963ef2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx @@ -0,0 +1,74 @@ +import { NumberFilter, NumberFilterCondition, useReadOnly } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import { TextField } from '@mui/material'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function NumberFilterMenu({ filter }: { filter: NumberFilter }) { + const { t } = useTranslation(); + const readOnly = useReadOnly(); + const conditions = useMemo(() => { + return [ + { + value: NumberFilterCondition.Equal, + text: t('grid.numberFilter.equal'), + }, + { + value: NumberFilterCondition.NotEqual, + text: t('grid.numberFilter.notEqual'), + }, + { + value: NumberFilterCondition.GreaterThan, + text: t('grid.numberFilter.greaterThan'), + }, + { + value: NumberFilterCondition.LessThan, + text: t('grid.numberFilter.lessThan'), + }, + { + value: NumberFilterCondition.GreaterThanOrEqualTo, + text: t('grid.numberFilter.greaterThanOrEqualTo'), + }, + { + value: NumberFilterCondition.LessThanOrEqualTo, + text: t('grid.numberFilter.lessThanOrEqualTo'), + }, + { + value: NumberFilterCondition.NumberIsEmpty, + text: t('grid.textFilter.isEmpty'), + }, + { + value: NumberFilterCondition.NumberIsNotEmpty, + text: t('grid.textFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displayTextField = useMemo(() => { + return ![NumberFilterCondition.NumberIsEmpty, NumberFilterCondition.NumberIsNotEmpty].includes(filter.condition); + }, [filter.condition]); + + return ( +
+ + {displayTextField && ( + + )} +
+ ); +} + +export default NumberFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx new file mode 100644 index 0000000000..217ad8d1ae --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx @@ -0,0 +1,48 @@ +import { SelectOptionFilter, SelectOptionFilterCondition } from '@/application/database-yjs'; +import { SelectOptionList } from '@/components/database/components/field/select-option'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function SingleSelectOptionFilterMenu({ filter }: { filter: SelectOptionFilter }) { + const { t } = useTranslation(); + const conditions = useMemo(() => { + return [ + { + value: SelectOptionFilterCondition.OptionIs, + text: t('grid.selectOptionFilter.is'), + }, + { + value: SelectOptionFilterCondition.OptionIsNot, + text: t('grid.selectOptionFilter.isNot'), + }, + { + value: SelectOptionFilterCondition.OptionIsEmpty, + text: t('grid.selectOptionFilter.isEmpty'), + }, + { + value: SelectOptionFilterCondition.OptionIsNotEmpty, + text: t('grid.selectOptionFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displaySelectOptionList = useMemo(() => { + return ![SelectOptionFilterCondition.OptionIsEmpty, SelectOptionFilterCondition.OptionIsNotEmpty].includes( + filter.condition + ); + }, [filter.condition]); + + return ( +
+ + {displaySelectOptionList && } +
+ ); +} + +export default SingleSelectOptionFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx new file mode 100644 index 0000000000..f3ca7690af --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx @@ -0,0 +1,74 @@ +import { TextFilter, TextFilterCondition, useReadOnly } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import { TextField } from '@mui/material'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function TextFilterMenu({ filter }: { filter: TextFilter }) { + const { t } = useTranslation(); + const readOnly = useReadOnly(); + const conditions = useMemo(() => { + return [ + { + value: TextFilterCondition.TextContains, + text: t('grid.textFilter.contains'), + }, + { + value: TextFilterCondition.TextDoesNotContain, + text: t('grid.textFilter.doesNotContain'), + }, + { + value: TextFilterCondition.TextStartsWith, + text: t('grid.textFilter.startWith'), + }, + { + value: TextFilterCondition.TextEndsWith, + text: t('grid.textFilter.endsWith'), + }, + { + value: TextFilterCondition.TextIs, + text: t('grid.textFilter.is'), + }, + { + value: TextFilterCondition.TextIsNot, + text: t('grid.textFilter.isNot'), + }, + { + value: TextFilterCondition.TextIsEmpty, + text: t('grid.textFilter.isEmpty'), + }, + { + value: TextFilterCondition.TextIsNotEmpty, + text: t('grid.textFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displayTextField = useMemo(() => { + return ![TextFilterCondition.TextIsEmpty, TextFilterCondition.TextIsNotEmpty].includes(filter.condition); + }, [filter.condition]); + + return ( +
+ + {displayTextField && ( + + )} +
+ ); +} + +export default TextFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts new file mode 100644 index 0000000000..fc54ea0f3a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts @@ -0,0 +1 @@ +export * from './FilterMenu'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/index.ts b/frontend/appflowy_web_app/src/components/database/components/filters/index.ts new file mode 100644 index 0000000000..c7b59bcd2f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/index.ts @@ -0,0 +1 @@ +export * from './Filters'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx new file mode 100644 index 0000000000..d3a30e1844 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx @@ -0,0 +1,51 @@ +import { DateFilter, DateFilterCondition } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import dayjs from 'dayjs'; + +function DateFilterContentOverview({ filter }: { filter: DateFilter }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!filter.timestamp) return ''; + + let startStr = ''; + let endStr = ''; + + if (filter.start) { + const end = filter.end ?? filter.start; + const moreThanOneYear = dayjs.unix(end).diff(dayjs.unix(filter.start), 'year') > 1; + const format = moreThanOneYear ? 'MMM D, YYYY' : 'MMM D'; + + startStr = dayjs.unix(filter.start).format(format); + endStr = dayjs.unix(end).format(format); + } + + const timestamp = dayjs.unix(filter.timestamp).format('MMM D'); + + switch (filter.condition) { + case DateFilterCondition.DateIs: + return `: ${timestamp}`; + case DateFilterCondition.DateBefore: + return `: ${t('grid.dateFilter.choicechipPrefix.before')} ${timestamp}`; + case DateFilterCondition.DateAfter: + return `: ${t('grid.dateFilter.choicechipPrefix.after')} ${timestamp}`; + case DateFilterCondition.DateOnOrBefore: + return `: ${t('grid.dateFilter.choicechipPrefix.onOrBefore')} ${timestamp}`; + case DateFilterCondition.DateOnOrAfter: + return `: ${t('grid.dateFilter.choicechipPrefix.onOrAfter')} ${timestamp}`; + case DateFilterCondition.DateWithIn: + return `: ${startStr} - ${endStr}`; + case DateFilterCondition.DateIsEmpty: + return `: ${t('grid.dateFilter.choicechipPrefix.isEmpty')}`; + case DateFilterCondition.DateIsNotEmpty: + return `: ${t('grid.dateFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [filter, t]); + + return <>{value}; +} + +export default DateFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx new file mode 100644 index 0000000000..9f6d1ea188 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx @@ -0,0 +1,59 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { + CheckboxFilterCondition, + ChecklistFilterCondition, + FieldType, + Filter, + SelectOptionFilter, + useFieldSelector, +} from '@/application/database-yjs'; +import DateFilterContentOverview from '@/components/database/components/filters/overview/DateFilterContentOverview'; +import NumberFilterContentOverview from '@/components/database/components/filters/overview/NumberFilterContentOverview'; +import SelectFilterContentOverview from '@/components/database/components/filters/overview/SelectFilterContentOverview'; +import TextFilterContentOverview from '@/components/database/components/filters/overview/TextFilterContentOverview'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function FilterContentOverview({ filter }: { filter: Filter }) { + const { field } = useFieldSelector(filter?.fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + const { t } = useTranslation(); + + return useMemo(() => { + if (!field) return null; + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return ; + case FieldType.Number: + return ; + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return ; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return ; + case FieldType.Checkbox: + return ( + <> + : {t('grid.checkboxFilter.choicechipPrefix.is')}{' '} + {filter.condition === CheckboxFilterCondition.IsChecked + ? t('grid.checkboxFilter.isChecked') + : t('grid.checkboxFilter.isUnchecked')} + + ); + case FieldType.Checklist: + return ( + <> + :{' '} + {filter.condition === ChecklistFilterCondition.IsComplete + ? t('grid.checklistFilter.isComplete') + : t('grid.checklistFilter.isIncomplted')} + + ); + default: + return null; + } + }, [field, fieldType, filter, t]); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx new file mode 100644 index 0000000000..64864541e7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx @@ -0,0 +1,38 @@ +import { NumberFilter, NumberFilterCondition } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function NumberFilterContentOverview({ filter }: { filter: NumberFilter }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!filter.content) { + return ''; + } + + const content = parseInt(filter.content); + + switch (filter.condition) { + case NumberFilterCondition.Equal: + return `= ${content}`; + case NumberFilterCondition.NotEqual: + return `!= ${content}`; + case NumberFilterCondition.GreaterThan: + return `> ${content}`; + case NumberFilterCondition.GreaterThanOrEqualTo: + return `>= ${content}`; + case NumberFilterCondition.LessThan: + return `< ${content}`; + case NumberFilterCondition.LessThanOrEqualTo: + return `<= ${content}`; + case NumberFilterCondition.NumberIsEmpty: + return t('grid.textFilter.isEmpty'); + case NumberFilterCondition.NumberIsNotEmpty: + return t('grid.textFilter.isNotEmpty'); + } + }, [filter.condition, filter.content, t]); + + return <>{value}; +} + +export default NumberFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx new file mode 100644 index 0000000000..64e8ddc00c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx @@ -0,0 +1,42 @@ +import { YDatabaseField } from '@/application/collab.type'; +import { + parseSelectOptionTypeOptions, + SelectOptionFilter, + SelectOptionFilterCondition, +} from '@/application/database-yjs'; +import React, { useMemo } from 'react'; + +import { useTranslation } from 'react-i18next'; + +function SelectFilterContentOverview({ filter, field }: { filter: SelectOptionFilter; field: YDatabaseField }) { + const typeOption = parseSelectOptionTypeOptions(field); + const { t } = useTranslation(); + const value = useMemo(() => { + if (!filter.optionIds?.length) return ''; + + const options = filter.optionIds + .map((optionId) => { + const option = typeOption?.options?.find((option) => option.id === optionId); + + return option?.name; + }) + .join(', '); + + switch (filter.condition) { + case SelectOptionFilterCondition.OptionIs: + return `: ${options}`; + case SelectOptionFilterCondition.OptionIsNot: + return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${options}`; + case SelectOptionFilterCondition.OptionIsEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; + case SelectOptionFilterCondition.OptionIsNotEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [filter.condition, filter.optionIds, t, typeOption?.options]); + + return <>{value}; +} + +export default SelectFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx new file mode 100644 index 0000000000..fc03b39c96 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx @@ -0,0 +1,33 @@ +import { TextFilter, TextFilterCondition } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function TextFilterContentOverview({ filter }: { filter: TextFilter }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!filter.content) return ''; + switch (filter.condition) { + case TextFilterCondition.TextContains: + case TextFilterCondition.TextIs: + return `: ${filter.content}`; + case TextFilterCondition.TextDoesNotContain: + case TextFilterCondition.TextIsNot: + return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${filter.content}`; + case TextFilterCondition.TextStartsWith: + return `: ${t('grid.textFilter.choicechipPrefix.startWith')} ${filter.content}`; + case TextFilterCondition.TextEndsWith: + return `: ${t('grid.textFilter.choicechipPrefix.endWith')} ${filter.content}`; + case TextFilterCondition.TextIsEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; + case TextFilterCondition.TextIsNotEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [t, filter]); + + return <>{value}; +} + +export default TextFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts b/frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts new file mode 100644 index 0000000000..47e041409e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts @@ -0,0 +1 @@ +export * from './FilterContentOverview'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/package.json b/frontend/appflowy_web_app/src/components/database/components/filters/package.json new file mode 100644 index 0000000000..e56f3198c9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/package.json @@ -0,0 +1,14 @@ +{ + "name": "filters", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/qinluhe/AppFlowy.git" + }, + "private": true +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx new file mode 100644 index 0000000000..b9a5017b38 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx @@ -0,0 +1,64 @@ +import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; +import { useRowMeta } from '@/application/database-yjs'; +import { useFieldSelector } from '@/application/database-yjs/selector'; +import { Cell } from '@/components/database/components/cell'; +import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse'; +import React, { useEffect, useState } from 'react'; + +export interface GridCellProps { + rowId: string; + fieldId: FieldId; + columnIndex: number; + rowIndex: number; + onResize?: (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => void; +} + +export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: GridCellProps) { + const ref = React.useRef(null); + const field = useFieldSelector(fieldId); + const row = useRowMeta(rowId); + const cell = row?.get(YjsDatabaseKey.cells)?.get(fieldId); + const [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined)); + + useEffect(() => { + if (!cell) return; + setCellValue(parseYDatabaseCellToCell(cell)); + const observerEvent = () => setCellValue(parseYDatabaseCellToCell(cell)); + + cell.observe(observerEvent); + + return () => { + cell.unobserve(observerEvent); + }; + }, [cell]); + + useEffect(() => { + const el = ref.current; + + if (!el) return; + + const observer = new ResizeObserver(() => { + if (onResize) { + onResize(rowIndex, columnIndex, { + width: el.offsetWidth, + height: el.offsetHeight, + }); + } + }); + + observer.observe(el); + + return () => { + observer.disconnect(); + }; + }, [columnIndex, onResize, rowIndex]); + + if (!field) return null; + return ( +
+ +
+ ); +} + +export default GridCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid-cell/index.ts new file mode 100644 index 0000000000..2b6d663ef5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-cell/index.ts @@ -0,0 +1 @@ +export * from './GridCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx new file mode 100644 index 0000000000..88c6fae84e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx @@ -0,0 +1,35 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { Column, useFieldSelector } from '@/application/database-yjs/selector'; +import { FieldTypeIcon } from '@/components/database/components/field'; +import React, { useMemo } from 'react'; + +export function GridColumn({ column, index }: { column: Column; index: number }) { + const { field } = useFieldSelector(column.fieldId); + const name = field?.get(YjsDatabaseKey.name); + const type = useMemo(() => { + const type = field?.get(YjsDatabaseKey.type); + + if (!type) return FieldType.RichText; + + return parseInt(type) as FieldType; + }, [field]); + + return ( +
+
+ +
+
{name}
+
+ ); +} + +export default GridColumn; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts new file mode 100644 index 0000000000..6de83c7026 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts @@ -0,0 +1,2 @@ +export * from './GridColumn'; +export * from './useRenderColumns'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx new file mode 100644 index 0000000000..c0041b5c5e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx @@ -0,0 +1,73 @@ +import { FieldId } from '@/application/collab.type'; +import { FieldVisibility } from '@/application/database-yjs/database.type'; +import { useGridColumnsSelector } from '@/application/database-yjs/selector'; +import { useCallback, useMemo } from 'react'; + +export enum GridColumnType { + Action, + Field, + NewProperty, +} + +const defaultVisibilitys = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty]; + +export type RenderColumn = { + type: GridColumnType; + visibility?: FieldVisibility; + fieldId?: FieldId; + width: number; + wrap?: boolean; +}; + +export function useRenderColumns(viewId: string) { + const columns = useGridColumnsSelector(viewId, defaultVisibilitys); + + console.log('columns', columns); + const renderColumns = useMemo(() => { + const fields = columns.map((column) => ({ + ...column, + type: GridColumnType.Field, + })); + + return [ + { + type: GridColumnType.Action, + width: 96, + }, + ...fields, + { + type: GridColumnType.NewProperty, + width: 150, + }, + { + type: GridColumnType.Action, + width: 96, + }, + ].filter(Boolean) as RenderColumn[]; + }, [columns]); + + const columnWidth = useCallback( + (index: number, containerWidth: number) => { + const { type, width } = renderColumns[index]; + + if (type === GridColumnType.NewProperty) { + const totalWidth = renderColumns.reduce((acc, column) => acc + column.width, 0); + const remainingWidth = containerWidth - totalWidth; + + return remainingWidth > 0 ? remainingWidth + width : width; + } + + if (type === GridColumnType.Action && containerWidth < 800) { + return 16; + } + + return width; + }, + [renderColumns] + ); + + return { + columns: renderColumns, + columnWidth, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx new file mode 100644 index 0000000000..64d0c39117 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx @@ -0,0 +1,73 @@ +import React, { useCallback, useEffect, useRef } from 'react'; +import { GridChildComponentProps, VariableSizeGrid } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { GridColumnType, RenderColumn, GridColumn } from '../grid-column'; + +export interface GridHeaderProps { + onScrollLeft: (left: number) => void; + columnWidth: (index: number, totalWidth: number) => number; + columns: RenderColumn[]; + scrollLeft?: number; +} + +export const GridHeader = ({ scrollLeft, onScrollLeft, columnWidth, columns }: GridHeaderProps) => { + const ref = useRef(null); + const Cell = useCallback(({ columnIndex, style, data }: GridChildComponentProps) => { + const column = data[columnIndex]; + + // Placeholder for Action toolbar + if (!column || column.type === GridColumnType.Action) return
; + + if (column.type === GridColumnType.Field) { + return ( +
+ +
+ ); + } + + return
; + }, []); + + useEffect(() => { + if (ref.current) { + ref.current.scrollTo({ scrollLeft }); + } + }, [scrollLeft]); + + useEffect(() => { + if (ref.current) { + ref.current?.resetAfterIndices({ columnIndex: 0, rowIndex: 0 }); + } + }, [columns]); + + return ( +
+ + {({ height, width }: { height: number; width: number }) => { + return ( + 36} + rowCount={1} + columnCount={columns.length} + columnWidth={(index) => columnWidth(index, width)} + ref={ref} + onScroll={(props) => { + onScrollLeft(props.scrollLeft); + }} + itemData={columns} + style={{ overscrollBehavior: 'none' }} + > + {Cell} + + ); + }} + +
+ ); +}; + +export default GridHeader; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts new file mode 100644 index 0000000000..44d8082bd7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts @@ -0,0 +1 @@ +export * from './GridHeader'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx new file mode 100644 index 0000000000..650ed3bfbe --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx @@ -0,0 +1,41 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { useDatabaseView } from '@/application/database-yjs'; +import { CalculationType } from '@/application/database-yjs/database.type'; +import { CalculationCell } from '@/components/database/components/calculation-cell'; +import { CalulationCell } from '@/components/database/components/calculation-cell/cell.type'; +import React, { useEffect, useState } from 'react'; + +export interface GridCalculateRowCellProps { + fieldId: string; +} + +export function GridCalculateRowCell({ fieldId }: GridCalculateRowCellProps) { + const calculations = useDatabaseView()?.get(YjsDatabaseKey.calculations); + const [calculation, setCalculation] = useState(); + + useEffect(() => { + if (!calculations) return; + const observerHandle = () => { + calculations.forEach((calculation) => { + if (calculation.get(YjsDatabaseKey.field_id) === fieldId) { + setCalculation({ + id: calculation.get(YjsDatabaseKey.id), + fieldId: calculation.get(YjsDatabaseKey.field_id), + value: calculation.get(YjsDatabaseKey.calculation_value), + type: Number(calculation.get(YjsDatabaseKey.type)) as CalculationType, + }); + } + }); + }; + + observerHandle(); + calculations.observeDeep(observerHandle); + + return () => { + calculations.unobserveDeep(observerHandle); + }; + }, [calculations, fieldId]); + return ; +} + +export default GridCalculateRowCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx new file mode 100644 index 0000000000..ef4be68406 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx @@ -0,0 +1,28 @@ +import { GridColumnType } from '@/components/database/components/grid-column'; +import React from 'react'; +import GridCell from 'src/components/database/components/grid-cell/GridCell'; + +export interface GridRowCellProps { + rowId: string; + fieldId?: string; + type: GridColumnType; + columnIndex: number; + rowIndex: number; + onResize?: (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => void; +} + +export function GridRowCell({ onResize, rowIndex, columnIndex, rowId, fieldId, type }: GridRowCellProps) { + if (type === GridColumnType.Field && fieldId) { + return ( + + ); + } + + if (type === GridColumnType.Action) { + return null; + } + + return null; +} + +export default GridRowCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts new file mode 100644 index 0000000000..365c3f467e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts @@ -0,0 +1,3 @@ +export * from './GridCalculateRowCell'; +export * from './GridRowCell'; +export * from './useRenderRows'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx new file mode 100644 index 0000000000..e5038cafff --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx @@ -0,0 +1,44 @@ +import { useReadOnly } from '@/application/database-yjs'; +import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const'; +import { useGridRowsSelector } from '@/application/database-yjs/selector'; +import { useMemo } from 'react'; + +export enum RenderRowType { + Row = 'row', + NewRow = 'new-row', + CalculateRow = 'calculate-row', +} + +export type RenderRow = { + type: RenderRowType; + rowId?: string; + height?: number; +}; + +export function useRenderRows() { + const rows = useGridRowsSelector(); + const readOnly = useReadOnly(); + + const renderRows = useMemo(() => { + return [ + ...rows.map((row) => ({ + type: RenderRowType.Row, + rowId: row.id, + height: row.height, + })), + + !readOnly && { + type: RenderRowType.NewRow, + height: DEFAULT_ROW_HEIGHT, + }, + { + type: RenderRowType.CalculateRow, + height: DEFAULT_ROW_HEIGHT, + }, + ].filter(Boolean) as RenderRow[]; + }, [readOnly, rows]); + + return { + rows: renderRows, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx new file mode 100644 index 0000000000..dd3ed13bfe --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx @@ -0,0 +1,177 @@ +import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const'; +import { AFScroller } from '@/components/_shared/scroller'; +import { GridColumnType, RenderColumn } from '@/components/database/components/grid-column'; +import { + GridCalculateRowCell, + GridRowCell, + RenderRowType, + useRenderRows, +} from '@/components/database/components/grid-row'; +import React, { useCallback, useEffect, useRef } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { GridChildComponentProps, VariableSizeGrid } from 'react-window'; + +export interface GridTableProps { + onScrollLeft: (left: number) => void; + columnWidth: (index: number, totalWidth: number) => number; + + columns: RenderColumn[]; + scrollLeft?: number; + viewId: string; +} + +export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: GridTableProps) => { + const ref = useRef(null); + const { rows } = useRenderRows(); + const rowHeights = useRef<{ [key: string]: number }>({}); + + useEffect(() => { + if (ref.current) { + console.log(ref.current, scrollLeft); + ref.current.scrollTo({ scrollLeft }); + } + }, [scrollLeft]); + + useEffect(() => { + if (ref.current) { + ref.current.resetAfterIndices({ columnIndex: 0, rowIndex: 0 }); + } + }, [columns]); + + const rowHeight = useCallback( + (index: number) => { + const row = rows[index]; + + if (!row || !row.rowId) return DEFAULT_ROW_HEIGHT; + + return rowHeights.current[row.rowId] || DEFAULT_ROW_HEIGHT; + }, + [rows] + ); + + const setRowHeight = useCallback( + (index: number, height: number) => { + const row = rows[index]; + const rowId = row.rowId; + + if (!row || !rowId) return; + const oldHeight = rowHeights.current[rowId]; + + rowHeights.current[rowId] = Math.max(oldHeight || DEFAULT_ROW_HEIGHT, height); + if (oldHeight !== height) { + ref.current?.resetAfterRowIndex(index, true); + } + }, + [rows] + ); + + const onResize = useCallback( + (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => { + setRowHeight(rowIndex, size.height); + }, + [setRowHeight] + ); + + const getItemKey = useCallback( + ({ columnIndex, rowIndex }: { columnIndex: number; rowIndex: number }) => { + const row = rows[rowIndex]; + const column = columns[columnIndex]; + const fieldId = column.fieldId; + + if (row.type === RenderRowType.Row) { + if (fieldId) { + return `${row.rowId}:${fieldId}`; + } + + return `${rowIndex}:${columnIndex}`; + } + + if (fieldId) { + return `${row.type}:${fieldId}`; + } + + return `${rowIndex}:${columnIndex}`; + }, + [columns, rows] + ); + const Cell = useCallback( + ({ columnIndex, rowIndex, style, data }: GridChildComponentProps) => { + const row = data.rows[rowIndex]; + const column = data.columns[columnIndex] as RenderColumn; + + const classList = ['flex', 'items-center', 'overflow-hidden']; + + if (column.wrap) { + classList.push('whitespace-pre-wrap', 'break-words'); + } else { + classList.push('whitespace-nowrap'); + } + + if (column.type === GridColumnType.Field) { + classList.push('border-b', 'border-l', 'border-line-divider', 'px-2'); + } + + if (column.type === GridColumnType.NewProperty) { + classList.push('border-b', 'border-line-divider', 'px-2'); + } + + if (row.type === RenderRowType.Row) { + return ( +
+ +
+ ); + } + + if (row.type === RenderRowType.CalculateRow && column.fieldId) { + return ( +
+ +
+ ); + } + + return
; + }, + [onResize] + ); + + return ( + + {({ height, width }: { height: number; width: number }) => ( + onScrollLeft(scrollLeft)} + rowCount={rows.length} + columnCount={columns.length} + columnWidth={(index) => columnWidth(index, width)} + rowHeight={rowHeight} + overscanRowCount={5} + overscanColumnCount={5} + style={{ + overscrollBehavior: 'none', + }} + itemKey={getItemKey} + itemData={{ columns, rows }} + outerElementType={AFScroller} + > + {Cell} + + )} + + ); +}; + +export default GridTable; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts new file mode 100644 index 0000000000..49518fa391 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts @@ -0,0 +1 @@ +export * from './GridTable'; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx new file mode 100644 index 0000000000..37575224ac --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx @@ -0,0 +1,20 @@ +import { useSortSelector } from '@/application/database-yjs'; +import SortCondition from '@/components/database/components/sorts/SortCondition'; +import React from 'react'; +import { FieldDisplay } from 'src/components/database/components/field'; + +function Sort({ sortId }: { sortId: string }) { + const sort = useSortSelector(sortId); + + if (!sort) return null; + return ( +
+
+ +
+ +
+ ); +} + +export default Sort; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx new file mode 100644 index 0000000000..78457da1ca --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx @@ -0,0 +1,30 @@ +import { Sort } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; + +function SortCondition({ sort }: { sort: Sort }) { + const condition = sort.condition; + const { t } = useTranslation(); + const conditionText = useMemo(() => { + switch (condition) { + case 0: + return t('grid.sort.ascending'); + case 1: + return t('grid.sort.descending'); + } + }, [condition, t]); + + return ( +
+ {conditionText} + +
+ ); +} + +export default SortCondition; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx new file mode 100644 index 0000000000..a657b4a0b9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx @@ -0,0 +1,17 @@ +import { useSortsSelector } from '@/application/database-yjs'; +import Sort from '@/components/database/components/sorts/Sort'; +import React from 'react'; + +function SortList() { + const sorts = useSortsSelector(); + + return ( +
+ {sorts.map((sortId) => ( + + ))} +
+ ); +} + +export default SortList; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx new file mode 100644 index 0000000000..a00aeea20c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx @@ -0,0 +1,43 @@ +import { useSortsSelector } from '@/application/database-yjs'; +import { Popover } from '@/components/_shared/popover'; +import SortList from '@/components/database/components/sorts/SortList'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as SortSvg } from '$icons/16x/sort_ascending.svg'; +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; + +export function Sorts() { + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const sorts = useSortsSelector(); + + if (sorts.length === 0) return null; + return ( + <> +
{ + setAnchorEl(e.currentTarget); + }} + className='flex cursor-pointer items-center gap-1 rounded-full border border-line-divider px-2 py-1 text-xs hover:border-fill-default hover:text-fill-default hover:shadow-sm' + > + + {t('grid.settings.sort')} + +
+ {open && ( + { + setAnchorEl(null); + }} + > + + + )} + + ); +} + +export default Sorts; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/index.ts b/frontend/appflowy_web_app/src/components/database/components/sorts/index.ts new file mode 100644 index 0000000000..467acd9081 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/index.ts @@ -0,0 +1 @@ +export * from './Sorts'; diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx new file mode 100644 index 0000000000..65a1b238bb --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx @@ -0,0 +1,97 @@ +import { ViewLayout, YjsFolderKey, YView } from '@/application/collab.type'; +import { useFolderContext } from '@/application/folder-yjs'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { DatabaseActions } from '@/components/database/components/conditions'; +import { forwardRef, FunctionComponent, SVGProps, useCallback, useEffect, useMemo } from 'react'; +import { ViewTabs, ViewTab } from './ViewTabs'; +import { useTranslation } from 'react-i18next'; + +import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; +import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; +import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; +import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg'; + +export interface DatabaseTabBarProps { + viewIds: string[]; + selectedViewId?: string; + setSelectedViewId?: (viewId: string) => void; +} + +const DatabaseIcons: { + [key in ViewLayout]: FunctionComponent & { title?: string | undefined }>; +} = { + [ViewLayout.Document]: DocumentSvg, + [ViewLayout.Grid]: GridSvg, + [ViewLayout.Board]: BoardSvg, + [ViewLayout.Calendar]: CalendarSvg, +}; + +export const DatabaseTabs = forwardRef( + ({ viewIds, selectedViewId, setSelectedViewId }, ref) => { + const objectId = useId().objectId; + const { t } = useTranslation(); + const folder = useFolderContext(); + const handleChange = (_: React.SyntheticEvent, newValue: string) => { + setSelectedViewId?.(newValue); + }; + + useEffect(() => { + if (selectedViewId === undefined) { + setSelectedViewId?.(objectId); + } + }, [selectedViewId, setSelectedViewId, objectId]); + const isSelected = useMemo(() => viewIds.some((viewId) => viewId === selectedViewId), [viewIds, selectedViewId]); + + const getFolderView = useCallback( + (viewId: string) => { + if (!folder) return null; + return folder.get(YjsFolderKey.views)?.get(viewId) as YView | null; + }, + [folder] + ); + + if (viewIds.length === 0) return null; + return ( +
+
+ + {viewIds.map((viewId, index) => { + const view = getFolderView(viewId); + + if (!view) return null; + const layout = Number(view.get(YjsFolderKey.layout)) as ViewLayout; + const Icon = DatabaseIcons[layout]; + const name = view.get(YjsFolderKey.name); + + return ( + } + iconPosition='start' + color='inherit' + label={name || t('grid.title.placeholder')} + value={viewId} + /> + ); + })} + +
+ +
+ ); + } +); diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx new file mode 100644 index 0000000000..7bbf91cf65 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx @@ -0,0 +1,18 @@ +import { Button, ButtonProps, styled } from '@mui/material'; + +export const TextButton = styled((props: ButtonProps) => ( +
diff --git a/frontend/appflowy_web_app/src/components/layout/layout.scss b/frontend/appflowy_web_app/src/components/layout/layout.scss index 4133489130..bac3baae69 100644 --- a/frontend/appflowy_web_app/src/components/layout/layout.scss +++ b/frontend/appflowy_web_app/src/components/layout/layout.scss @@ -35,6 +35,7 @@ .appflowy-scroll-container { &::-webkit-scrollbar { width: 0; + height: 0; } } @@ -44,20 +45,14 @@ opacity: 60%; } -.workspaces { - ::-webkit-scrollbar { - width: 0px; - } -} - - -.MuiPopover-root, .MuiPaper-root { +.workspaces, .database-conditions, .grid-scroll-table { ::-webkit-scrollbar { width: 0; height: 0; } } + .view-icon { @apply flex w-fit cursor-pointer rounded-lg py-2 text-6xl; font-family: "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols; diff --git a/frontend/appflowy_web_app/src/pages/DatabasePage.tsx b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx new file mode 100644 index 0000000000..e1fbfb8067 --- /dev/null +++ b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx @@ -0,0 +1,10 @@ +import { Database } from '@/components/database'; +import React from 'react'; + +function DatabasePage () { + return ( + + ); +} + +export default DatabasePage; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/pages/ProductPage.tsx b/frontend/appflowy_web_app/src/pages/ProductPage.tsx index 8080e339ef..f7b5615a77 100644 --- a/frontend/appflowy_web_app/src/pages/ProductPage.tsx +++ b/frontend/appflowy_web_app/src/pages/ProductPage.tsx @@ -1,34 +1,44 @@ import { CollabType } from '@/application/collab.type'; import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; +import DatabasePage from '@/pages/DatabasePage'; import React, { useMemo } from 'react'; import { useParams } from 'react-router-dom'; import DocumentPage from '@/pages/DocumentPage'; enum URL_COLLAB_TYPE { DOCUMENT = 'document', - DATABASE = 'database', + GRID = 'grid', + BOARD = 'board', + CALENDAR = 'calendar', } const collabTypeMap: Record = { [URL_COLLAB_TYPE.DOCUMENT]: CollabType.Document, - [URL_COLLAB_TYPE.DATABASE]: CollabType.Database, + [URL_COLLAB_TYPE.GRID]: CollabType.WorkspaceDatabase, + [URL_COLLAB_TYPE.BOARD]: CollabType.WorkspaceDatabase, + [URL_COLLAB_TYPE.CALENDAR]: CollabType.WorkspaceDatabase, }; function ProductPage() { - const { workspaceId, collabType, objectId } = useParams(); + const { workspaceId, type, objectId } = useParams(); const PageComponent = useMemo(() => { - switch (collabType) { + switch (type) { case URL_COLLAB_TYPE.DOCUMENT: return DocumentPage; + case URL_COLLAB_TYPE.GRID: + case URL_COLLAB_TYPE.BOARD: + case URL_COLLAB_TYPE.CALENDAR: + return DatabasePage; default: return null; } - }, [collabType]); + }, [type]); - if (!workspaceId || !collabType || !objectId) return null; + console.log(workspaceId, type, objectId); + if (!workspaceId || !type || !objectId) return null; return ( - + {PageComponent && } ); diff --git a/frontend/appflowy_web_app/src/styles/variables/dark.variables.css b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css index b82d97e5be..6753969ca0 100644 --- a/frontend/appflowy_web_app/src/styles/variables/dark.variables.css +++ b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css @@ -1,12 +1,12 @@ /** * Do not edit directly -* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated on Thu, 09 May 2024 03:26:45 GMT * Generated from $pnpm css:variables */ :root[data-dark-mode=true] { --base-light-neutral-50: #f9fafd; - --base-light-neutral-100: #edeef2; + --base-light-neutral-100: #e5e5e5; --base-light-neutral-200: #e2e4eb; --base-light-neutral-300: #f2f2f2; --base-light-neutral-400: #e0e0e0; diff --git a/frontend/appflowy_web_app/src/styles/variables/light.variables.css b/frontend/appflowy_web_app/src/styles/variables/light.variables.css index 0477655f66..b1494114bd 100644 --- a/frontend/appflowy_web_app/src/styles/variables/light.variables.css +++ b/frontend/appflowy_web_app/src/styles/variables/light.variables.css @@ -1,12 +1,12 @@ /** * Do not edit directly -* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated on Thu, 09 May 2024 03:26:45 GMT * Generated from $pnpm css:variables */ :root { --base-light-neutral-50: #f9fafd; - --base-light-neutral-100: #edeef2; + --base-light-neutral-100: #e5e5e5; --base-light-neutral-200: #e2e4eb; --base-light-neutral-300: #f2f2f2; --base-light-neutral-400: #e0e0e0; @@ -83,7 +83,7 @@ --icon-disabled: #e0e0e0; --icon-on-toolbar: #ffffff; --line-border: #bdbdbd; - --line-divider: #edeef2; + --line-divider: #e5e5e5; --line-on-toolbar: #4f4f4f; --fill-toolbar: #333333; --fill-default: #00bcf0; @@ -91,7 +91,7 @@ --fill-pressed: #009fd1; --fill-active: #e0f8ff; --fill-list-hover: #e0f8ff; - --fill-list-active: #edeef2; + --fill-list-active: #f9fafd; --content-blue-400: #00bcf0; --content-blue-300: #52d1f4; --content-blue-600: #009fd1; @@ -120,5 +120,5 @@ --tint-yellow: #fff2cd; --shadow: 0px 0px 10px 0px rgba(0,0,0,0.1); --scrollbar-thumb: #bdbdbd; - --scrollbar-track: #edeef2; + --scrollbar-track: #e5e5e5; } \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/utils/time.ts b/frontend/appflowy_web_app/src/utils/time.ts index 3b6920fb34..792b72ee61 100644 --- a/frontend/appflowy_web_app/src/utils/time.ts +++ b/frontend/appflowy_web_app/src/utils/time.ts @@ -1,10 +1,6 @@ import dayjs from 'dayjs'; -export enum DateFormat { - Date = 'MMM D, YYYY', - DateTime = 'MMM D, YYYY h:mm A', -} - -export function renderDate(date: string, format: DateFormat = DateFormat.Date): string { +export function renderDate(date: string, format: string, isUnix?: boolean): string { + if (isUnix) return dayjs.unix(Number(date)).format(format); return dayjs(date).format(format); } diff --git a/frontend/appflowy_web_app/src/utils/url.ts b/frontend/appflowy_web_app/src/utils/url.ts index 8d67f3583f..a10cf9ca85 100644 --- a/frontend/appflowy_web_app/src/utils/url.ts +++ b/frontend/appflowy_web_app/src/utils/url.ts @@ -1,12 +1,14 @@ import { getPlatform } from '@/utils/platform'; -import validator from 'validator'; +import isURL from 'validator/lib/isURL'; +import isIP from 'validator/lib/isIP'; +import isFQDN from 'validator/lib/isFQDN'; export const downloadPage = 'https://appflowy.io/download'; export const openAppFlowySchema = 'appflowy-flutter://'; export function isValidUrl(input: string) { - return validator.isURL(input, { require_protocol: true, require_host: false }); + return isURL(input, { require_protocol: true, require_host: false }); } // Process the URL to make sure it's a valid URL @@ -20,7 +22,7 @@ export function processUrl(input: string) { const domain = input.split('/')[0]; - if (validator.isIP(domain) || validator.isFQDN(domain)) { + if (isIP(domain) || isFQDN(domain)) { processedUrl = `https://${input}`; if (isValidUrl(processedUrl)) { return processedUrl; diff --git a/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs b/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs index 00647333e2..9de67fc1be 100644 --- a/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs +++ b/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs @@ -1,6 +1,6 @@ /** * Do not edit directly -* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated on Thu, 09 May 2024 03:26:45 GMT * Generated from $pnpm css:variables */ diff --git a/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs b/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs index 798741f06c..63e679a90a 100644 --- a/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs +++ b/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs @@ -1,6 +1,6 @@ /** * Do not edit directly -* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated on Thu, 09 May 2024 03:26:45 GMT * Generated from $pnpm css:variables */ diff --git a/frontend/appflowy_web_app/style-dictionary/tokens/base.json b/frontend/appflowy_web_app/style-dictionary/tokens/base.json index 4e31b0523d..f92d39267f 100644 --- a/frontend/appflowy_web_app/style-dictionary/tokens/base.json +++ b/frontend/appflowy_web_app/style-dictionary/tokens/base.json @@ -7,7 +7,7 @@ "type": "color" }, "100": { - "value": "#edeef2", + "value": "#e5e5e5", "type": "color" }, "200": { diff --git a/frontend/appflowy_web_app/tsconfig.json b/frontend/appflowy_web_app/tsconfig.json index de30c24901..05dcd8d587 100644 --- a/frontend/appflowy_web_app/tsconfig.json +++ b/frontend/appflowy_web_app/tsconfig.json @@ -27,7 +27,7 @@ "node", "jest" ], - "baseUrl": "./", + "baseUrl": ".", "paths": { "@/*": [ "src/*" @@ -37,6 +37,9 @@ ], "$client-services": [ "src/application/services/js-services" + ], + "$icons/*": [ + "../resources/flowy-flowy_icons/*" ] } }, diff --git a/frontend/appflowy_web_app/vite.config.ts b/frontend/appflowy_web_app/vite.config.ts index b2621799ed..0e4cfebb4b 100644 --- a/frontend/appflowy_web_app/vite.config.ts +++ b/frontend/appflowy_web_app/vite.config.ts @@ -5,7 +5,9 @@ import wasm from 'vite-plugin-wasm'; import { visualizer } from 'rollup-plugin-visualizer'; import usePluginImport from 'vite-plugin-importer'; import { totalBundleSize } from 'vite-plugin-total-bundle-size'; +import path from 'path'; +const resourcesPath = path.resolve(__dirname, '../resources'); const isDev = process.env.NODE_ENV === 'development'; // https://vitejs.dev/config/ export default defineConfig({ @@ -104,8 +106,8 @@ export default defineConfig({ id.includes('/react-is@') || id.includes('/yjs@') || id.includes('/y-indexeddb@') || - id.includes('/dexie@') || - id.includes('/redux') + id.includes('/redux') || + id.includes('/react-custom-scrollbars') ) { return 'common'; } @@ -124,6 +126,7 @@ export default defineConfig({ ? `${__dirname}/src/application/services/tauri-services` : `${__dirname}/src/application/services/js-services`, }, + { find: '$icons', replacement: `${resourcesPath}/flowy_icons/` }, ], },