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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