From a8b4f2270332b40a480cf8ca4a43086d440c7b03 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Mon, 22 Jul 2024 13:37:27 +0800 Subject: [PATCH] feat: support publish database on web (#5748) * fix: view name * fix: hidden login on web * fix: database update bugs * feat: support render database on web * fix: loading * fix: calendar width on mobile * fix: calendar boder color * fix: replace some icons * fix: deal with visible view ids * fix: filter error child * fix: hide filters and sorts * fix: the style of relation * fix: throw error when apply fail * fix: upgrade yjs * fix: eslint errors * fix: support group by checkbox * fix: add shortcut to clear data * fix: relation * fix: relation * fix: relation * fix: relation * fix: view meta * fix: view meta * fix: view meta * fix: empty database block * fix: 0716 bugs * fix: add button to url cell * fix: jest test * fix: unit tests * fix: lint * fix: reduce database space * fix: add after payment page * fix: add spacing --------- Co-authored-by: Lucas.Xu --- frontend/appflowy_web_app/deploy/server.ts | 14 ++ frontend/appflowy_web_app/package.json | 2 +- frontend/appflowy_web_app/pnpm-lock.yaml | 36 ++--- .../src/application/collab.type.ts | 11 +- .../database-yjs/__tests__/group.test.ts | 29 +++- .../database-yjs/__tests__/selector.test.tsx | 2 +- .../database-yjs/__tests__/withTestingData.ts | 7 +- .../src/application/database-yjs/const.ts | 3 +- .../src/application/database-yjs/context.ts | 19 ++- .../application/database-yjs/database.type.ts | 2 + .../fields/number/__tests__/format.test.ts | 2 +- .../database-yjs/fields/number/format.ts | 13 +- .../src/application/database-yjs/group.ts | 27 +++- .../src/application/database-yjs/selector.ts | 111 +++++++------- .../src/application/database-yjs/sort.ts | 2 +- .../src/application/db/index.ts | 40 ++++- .../src/application/db/tables/view_metas.ts | 1 + .../src/application/publish/context.tsx | 68 +++++++-- .../js-services/__tests__/index.test.ts | 36 +---- .../js-services/cache/__tests__/cache.test.ts | 29 +--- .../services/js-services/cache/index.ts | 90 +++++++----- .../application/services/js-services/index.ts | 32 ++-- .../services/js-services/wasm/client_api.ts | 11 +- .../src/application/services/services.type.ts | 2 +- .../services/tauri-services/index.ts | 16 +- .../application/slate-yjs/plugins/withYjs.ts | 8 +- .../application/slate-yjs/utils/convert.ts | 10 +- .../src/application/ydoc/apply/index.ts | 7 +- .../src/assets/ai_indicator.svg | 23 +++ .../src/assets/ai_summary.svg | 4 + .../src/assets/ai_translate.svg | 4 + .../appflowy_web_app/src/assets/checkbox.svg | 4 + .../appflowy_web_app/src/assets/checklist.svg | 4 + .../src/assets/created_at.svg | 12 ++ frontend/appflowy_web_app/src/assets/date.svg | 6 + .../src/assets/last_modified.svg | 4 + frontend/appflowy_web_app/src/assets/link.svg | 4 + .../src/assets/multiselect.svg | 8 + .../appflowy_web_app/src/assets/number.svg | 3 + .../appflowy_web_app/src/assets/relation.svg | 8 + .../src/assets/single_select.svg | 4 + frontend/appflowy_web_app/src/assets/text.svg | 4 + frontend/appflowy_web_app/src/assets/url.svg | 3 + .../_shared/scroller/AFScroller.tsx | 3 +- .../src/components/app/App.tsx | 2 + .../src/components/app/AppConfig.tsx | 18 +++ .../src/components/app/AppTheme.tsx | 7 +- .../src/components/database/Database.tsx | 135 +++++++++-------- .../src/components/database/DatabaseViews.tsx | 17 ++- .../database/__tests__/DatabaseRow.cy.tsx | 1 + .../__tests__/withTestingDatabase.tsx | 1 + .../src/components/database/board/Board.tsx | 1 + .../components/database/calendar/Calendar.tsx | 2 +- .../database/calendar/calendar.scss | 8 +- .../database/components/board/card/Card.tsx | 2 +- .../components/board/column/Column.tsx | 5 +- .../board/column/useRenderColumn.ts | 31 ---- .../board/column/useRenderColumn.tsx | 51 +++++++ .../database/components/board/group/Group.tsx | 4 +- .../components/calendar/event/Event.tsx | 21 +-- .../components/calendar/toolbar/NoDate.tsx | 16 +- .../components/calendar/toolbar/NoDateRow.tsx | 16 +- .../components/calendar/toolbar/Toolbar.tsx | 1 + .../components/cell/checkbox/CheckboxCell.tsx | 4 +- .../cell/checklist/ChecklistCell.tsx | 5 +- .../RowCreateModifiedTime.tsx | 2 +- .../components/cell/date/DateTimeCell.tsx | 2 +- .../components/cell/number/NumberCell.tsx | 3 + .../components/cell/primary/PrimaryCell.tsx | 26 ++-- .../cell/relation/RelationItems.tsx | 137 ++++++++++++++---- .../cell/relation/RelationPrimaryValue.tsx | 4 +- .../cell/select-option/SelectOptionCell.tsx | 5 +- .../database/components/cell/url/UrlCell.tsx | 42 ++++++ .../database/components/field/CardField.tsx | 45 ++++-- .../components/field/FieldTypeIcon.tsx | 27 ++-- .../grid/grid-column/GridColumn.tsx | 8 +- .../grid/grid-column/useRenderFields.tsx | 16 +- .../grid/grid-header/GridHeader.tsx | 18 ++- .../grid/grid-row/GridCalculateRowCell.tsx | 38 +++-- .../components/grid/grid-table/GridTable.tsx | 16 +- .../components/header/DatabaseHeader.tsx | 2 +- .../database/components/property/Property.tsx | 2 + .../components/property/PropertyWrapper.tsx | 4 +- .../database/components/tabs/DatabaseTabs.tsx | 15 +- .../src/components/document/Document.tsx | 22 +-- .../src/components/editor/EditorContext.tsx | 10 +- .../blocks/database/DatabaseBlock.tsx | 45 +++++- .../components/leaf/mention/MentionDate.tsx | 2 +- .../components/leaf/mention/MentionPage.tsx | 7 +- .../src/components/login/Login.tsx | 4 +- .../src/components/publish/CollabView.tsx | 45 +++--- .../src/components/publish/DatabaseView.tsx | 69 +++++++++ .../src/components/view-meta/ViewCover.tsx | 7 +- .../components/view-meta/ViewMetaPreview.tsx | 1 + .../src/pages/AfterPaymentPage.tsx | 41 ++++++ .../appflowy_web_app/src/styles/mixin.scss | 4 +- frontend/appflowy_web_app/src/utils/url.ts | 8 + frontend/resources/translations/en.json | 1 + 98 files changed, 1190 insertions(+), 564 deletions(-) create mode 100644 frontend/appflowy_web_app/src/assets/ai_indicator.svg create mode 100644 frontend/appflowy_web_app/src/assets/ai_summary.svg create mode 100644 frontend/appflowy_web_app/src/assets/ai_translate.svg create mode 100644 frontend/appflowy_web_app/src/assets/checkbox.svg create mode 100644 frontend/appflowy_web_app/src/assets/checklist.svg create mode 100644 frontend/appflowy_web_app/src/assets/created_at.svg create mode 100644 frontend/appflowy_web_app/src/assets/date.svg create mode 100644 frontend/appflowy_web_app/src/assets/last_modified.svg create mode 100644 frontend/appflowy_web_app/src/assets/link.svg create mode 100644 frontend/appflowy_web_app/src/assets/multiselect.svg create mode 100644 frontend/appflowy_web_app/src/assets/number.svg create mode 100644 frontend/appflowy_web_app/src/assets/relation.svg create mode 100644 frontend/appflowy_web_app/src/assets/single_select.svg create mode 100644 frontend/appflowy_web_app/src/assets/text.svg create mode 100644 frontend/appflowy_web_app/src/assets/url.svg delete mode 100644 frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.tsx create mode 100644 frontend/appflowy_web_app/src/components/publish/DatabaseView.tsx create mode 100644 frontend/appflowy_web_app/src/pages/AfterPaymentPage.tsx diff --git a/frontend/appflowy_web_app/deploy/server.ts b/frontend/appflowy_web_app/deploy/server.ts index 414cdb3e4d..f2fb31619f 100644 --- a/frontend/appflowy_web_app/deploy/server.ts +++ b/frontend/appflowy_web_app/deploy/server.ts @@ -68,6 +68,20 @@ const createServer = async (req: Request) => { logger.info(`Request URL: ${hostname}${reqUrl.pathname}`); + if (reqUrl.pathname === '/after-payment') { + timer(); + const htmlData = fs.readFileSync(indexPath, 'utf8'); + const $ = load(htmlData); + + $('title').text('Payment Success | AppFlowy'); + $('link[rel="icon"]').attr('href', '/appflowy.svg'); + setOrUpdateMetaTag($, 'meta[name="description"]', 'name', 'Payment success on AppFlowy'); + + return new Response($.html(), { + headers: { 'Content-Type': 'text/html' }, + }); + } + const [namespace, publishName] = reqUrl.pathname.slice(1).split('/'); logger.info(`Namespace: ${namespace}, Publish Name: ${publishName}`); diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index 0f7c566a6f..22bf0cb631 100644 --- a/frontend/appflowy_web_app/package.json +++ b/frontend/appflowy_web_app/package.json @@ -98,7 +98,7 @@ "validator": "^13.11.0", "vite-plugin-wasm": "^3.3.0", "y-indexeddb": "9.0.12", - "yjs": "^13.6.14" + "yjs": "14.0.0-1" }, "devDependencies": { "@babel/preset-env": "^7.24.7", diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index cbee7177c7..584d748c79 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@appflowyinc/client-api-wasm': specifier: 0.1.2 @@ -36,7 +40,7 @@ dependencies: version: 2.0.0(react-redux@8.1.3)(react@18.2.0) '@slate-yjs/core': specifier: ^1.0.2 - version: 1.0.2(slate@0.101.5)(yjs@13.6.15) + version: 1.0.2(slate@0.101.5)(yjs@14.0.0-1) '@tauri-apps/api': specifier: ^1.5.3 version: 1.5.6 @@ -222,10 +226,10 @@ dependencies: version: 3.3.0(vite@5.2.0) y-indexeddb: specifier: 9.0.12 - version: 9.0.12(yjs@13.6.15) + version: 9.0.12(yjs@14.0.0-1) yjs: - specifier: ^13.6.14 - version: 13.6.15 + specifier: 14.0.0-1 + version: 14.0.0-1 devDependencies: '@babel/preset-env': @@ -3589,15 +3593,15 @@ packages: dependencies: '@sinonjs/commons': 3.0.1 - /@slate-yjs/core@1.0.2(slate@0.101.5)(yjs@13.6.15): + /@slate-yjs/core@1.0.2(slate@0.101.5)(yjs@14.0.0-1): resolution: {integrity: sha512-X0hLFJbQu9c1ItWBaNuEn0pqcXYK76KCp8C4Gvy/VaTQVMo1VgAb2WiiJ0Je/AyuIYEPPSTNVOcyrGHwgA7e6Q==} peerDependencies: slate: '>=0.70.0' yjs: ^13.5.29 dependencies: slate: 0.101.5 - y-protocols: 1.0.6(yjs@13.6.15) - yjs: 13.6.15 + y-protocols: 1.0.6(yjs@14.0.0-1) + yjs: 14.0.0-1 dev: false /@svgr/babel-plugin-add-jsx-attribute@7.0.0(@babel/core@7.24.3): @@ -11556,24 +11560,24 @@ packages: engines: {node: '>=0.4'} dev: true - /y-indexeddb@9.0.12(yjs@13.6.15): + /y-indexeddb@9.0.12(yjs@14.0.0-1): resolution: {integrity: sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} peerDependencies: yjs: ^13.0.0 dependencies: lib0: 0.2.94 - yjs: 13.6.15 + yjs: 14.0.0-1 dev: false - /y-protocols@1.0.6(yjs@13.6.15): + /y-protocols@1.0.6(yjs@14.0.0-1): resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} peerDependencies: yjs: ^13.0.0 dependencies: lib0: 0.2.94 - yjs: 13.6.15 + yjs: 14.0.0-1 dev: false /y18n@4.0.3: @@ -11642,9 +11646,9 @@ packages: fd-slicer: 1.1.0 dev: true - /yjs@13.6.15: - resolution: {integrity: sha512-moFv4uNYhp8BFxIk3AkpoAnnjts7gwdpiG8RtyFiKbMtxKCS0zVZ5wPaaGpwC3V2N/K8TK8MwtSI3+WO9CHWjQ==} - engines: {node: '>=16.0.0', npm: '>=8.0.0'} + /yjs@14.0.0-1: + resolution: {integrity: sha512-w0iJlEx+XvkvPkdBH0L8pb4Da2DvTEA7UdDl/dOFCQfA0siT4cUtbJ8LfoiliH2juYFqdIoqxbScHakKBiIv0g==} + requiresBuild: true dependencies: lib0: 0.2.94 dev: false @@ -11662,7 +11666,3 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/frontend/appflowy_web_app/src/application/collab.type.ts b/frontend/appflowy_web_app/src/application/collab.type.ts index 27472acfcd..0040c49ffc 100644 --- a/frontend/appflowy_web_app/src/application/collab.type.ts +++ b/frontend/appflowy_web_app/src/application/collab.type.ts @@ -1,3 +1,4 @@ +import { ViewMeta } from '@/application/db/tables/view_metas'; import * as Y from 'yjs'; export type BlockId = string; @@ -413,7 +414,7 @@ export interface YDatabase extends Y.Map { get(key: YjsDatabaseKey.id): string; } -export interface YDatabaseViews extends Y.Map { +export interface YDatabaseViews extends Y.Map { get(key: ViewId): YDatabaseView; } @@ -558,7 +559,7 @@ export interface YDatabaseMetas extends Y.Map { get(key: YjsDatabaseKey.iid): string; } -export interface YDatabaseFields extends Y.Map { +export interface YDatabaseFields extends Y.Map { get(key: FieldId): YDatabaseField; } @@ -667,3 +668,9 @@ export interface PublishViewMetaData { child_views: PublishViewInfo[]; ancestor_views: PublishViewInfo[]; } + +export type GetViewRowsMap = (viewId: string) => Promise<{ rows: Y.Map; destroy: () => void }>; + +export type LoadView = (viewId: string) => Promise; + +export type LoadViewMeta = (viewId: string, onChange?: (meta: ViewMeta) => void) => Promise; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/group.test.ts b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/group.test.ts index adbe80aaa3..ab8a32beaa 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/group.test.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/group.test.ts @@ -24,10 +24,37 @@ describe('Database group', () => { const { fields, rowMap } = withTestingData(); expect(groupByField(rows, rowMap, fields.get('text_field'))).toBeUndefined(); expect(groupByField(rows, rowMap, fields.get('number_field'))).toBeUndefined(); - expect(groupByField(rows, rowMap, fields.get('checkbox_field'))).toBeUndefined(); expect(groupByField(rows, rowMap, fields.get('checklist_field'))).toBeUndefined(); }); + it('should gourp by checkbox field', () => { + const { fields, rowMap } = withTestingData(); + const field = fields.get('checkbox_field'); + const result = groupByField(rows, rowMap, field); + const expectRes = new Map([ + [ + 'Yes', + [ + { id: '1', height: 37 }, + { id: '3', height: 37 }, + { id: '5', height: 37 }, + { id: '7', height: 37 }, + { id: '9', height: 37 }, + ], + ], + [ + 'No', + [ + { id: '2', height: 37 }, + { id: '4', height: 37 }, + { id: '6', height: 37 }, + { id: '8', height: 37 }, + { id: '10', height: 37 }, + ], + ], + ]); + expect(result).toEqual(expectRes); + }); it('should group by select option field', () => { const { fields, rowMap } = withTestingData(); const field = fields.get('single_select_field'); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/selector.test.tsx b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/selector.test.tsx index 46473fae01..abfdce9c91 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/selector.test.tsx +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/selector.test.tsx @@ -30,7 +30,7 @@ const wrapperCreator = (viewId: string, doc: YDoc, rowDocMap: Y.Map) => ({ children }: { children: React.ReactNode }) => { return ( - + {children} ); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingData.ts b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingData.ts index 3ff4a32b12..5c54bb658e 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingData.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingData.ts @@ -1,6 +1,5 @@ import { YDatabase, - YDatabaseField, YDatabaseFields, YDatabaseFilters, YDatabaseGroup, @@ -133,11 +132,13 @@ export function withTestingDatabase(viewId: string) { const fieldOrder = new Y.Array(); const rowOrders = new Y.Array(); - Array.from(fields).forEach(([fieldId, field]) => { + fields.forEach((field) => { const setting = new Y.Map(); + const fieldId = field.get(YjsDatabaseKey.id); + if (fieldId === 'text_field') { - (field as YDatabaseField).set(YjsDatabaseKey.is_primary, true); + field.set(YjsDatabaseKey.is_primary, true); } fieldOrder.push([fieldId]); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/const.ts b/frontend/appflowy_web_app/src/application/database-yjs/const.ts index 436f28ef91..632642831e 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/const.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/const.ts @@ -4,10 +4,11 @@ import * as Y from 'yjs'; import { v5 as uuidv5, parse as uuidParse } from 'uuid'; export const DEFAULT_ROW_HEIGHT = 36; -export const MIN_COLUMN_WIDTH = 100; +export const MIN_COLUMN_WIDTH = 150; export const getCell = (rowId: string, fieldId: string, rowMetas: Y.Map) => { const rowMeta = rowMetas.get(rowId); + const meta = rowMeta?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; return meta?.get(YjsDatabaseKey.cells)?.get(fieldId); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/context.ts b/frontend/appflowy_web_app/src/application/database-yjs/context.ts index cbac5bbd45..c74a718bd2 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/context.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/context.ts @@ -1,18 +1,27 @@ -import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; -import { ViewMeta } from '@/application/db/tables/view_metas'; +import { + GetViewRowsMap, + LoadView, + LoadViewMeta, + YDatabase, + YDatabaseRow, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; import { createContext, useContext } from 'react'; import * as Y from 'yjs'; export interface DatabaseContextState { readOnly: boolean; databaseDoc: YDoc; + iidIndex: string; viewId: string; rowDocMap: Y.Map; isDatabaseRowPage?: boolean; navigateToRow?: (rowId: string) => void; - loadView?: (viewId: string) => Promise; - getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map; destroy: () => void }>; - loadViewMeta?: (viewId: string) => Promise; + loadView?: LoadView; + getViewRowsMap?: GetViewRowsMap; + loadViewMeta?: LoadViewMeta; navigateToView?: (viewId: string) => Promise; } 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 index c8ac7da5b0..f7c4dc3a03 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts @@ -18,6 +18,8 @@ export enum FieldType { LastEditedTime = 8, CreatedTime = 9, Relation = 10, + AISummaries = 11, + AITranslations = 12, } export enum CalculationType { 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 index e165752348..f80b1db220 100644 --- 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 @@ -50,7 +50,7 @@ describe('currencyFormaterMap', () => { 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']; + 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]); 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 index 589f6ac3ec..61e0942b01 100644 --- 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 @@ -32,11 +32,18 @@ export const currencyFormaterMap: Record string> = }) .format(n) .replace('$', 'CA$'), - [NumberFormat.EUR]: (n: number) => - new Intl.NumberFormat('en-IE', { + [NumberFormat.EUR]: (n: number) => { + const formattedAmount = new Intl.NumberFormat('de-DE', { ...commonProps, currency: 'EUR', - }).format(n), + }) + .format(n) + .replace('€', '') + .trim(); + + return `€${formattedAmount}`; + }, + [NumberFormat.Pound]: (n: number) => new Intl.NumberFormat('en-GB', { ...commonProps, diff --git a/frontend/appflowy_web_app/src/application/database-yjs/group.ts b/frontend/appflowy_web_app/src/application/database-yjs/group.ts index 709053aa32..7cd66ac9c6 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/group.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/group.ts @@ -9,8 +9,31 @@ export function groupByField(rows: Row[], rowMetas: Y.Map, field: YDatabas const fieldType = Number(field.get(YjsDatabaseKey.type)); const isSelectOptionField = [FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType); - if (!isSelectOptionField) return; - return groupBySelectOption(rows, rowMetas, field); + if (isSelectOptionField) { + return groupBySelectOption(rows, rowMetas, field); + } + + if (fieldType === FieldType.Checkbox) { + return groupByCheckbox(rows, rowMetas, field); + } + + return; +} + +export function groupByCheckbox(rows: Row[], rowMetas: Y.Map, field: YDatabaseField) { + const fieldId = field.get(YjsDatabaseKey.id); + const result = new Map(); + + rows.forEach((row) => { + const cellData = getCellData(row.id, fieldId, rowMetas); + + const groupName = cellData === 'Yes' ? 'Yes' : 'No'; + const group = result.get(groupName) ?? []; + + group.push(row); + result.set(groupName, group); + }); + return result; } export function groupBySelectOption(rows: Row[], rowMetas: Y.Map, field: YDatabaseField) { diff --git a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts index c1135ab747..3db157b2fb 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts @@ -1,4 +1,12 @@ -import { FieldId, SortId, YDatabaseField, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { + FieldId, + SortId, + YDatabase, + YDatabaseField, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; import { useDatabase, @@ -14,7 +22,7 @@ import { sortBy } from '@/application/database-yjs/sort'; import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse'; import { DateTimeCell } from '@/application/database-yjs/cell.type'; import * as dayjs from 'dayjs'; -import { throttle } from 'lodash-es'; +import { debounce } from 'lodash-es'; import { useCallback, useEffect, useMemo, useState } from 'react'; import Y from 'yjs'; import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMetaKey, SortCondition } from './database.type'; @@ -33,7 +41,7 @@ export interface Row { const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty]; -export function useDatabaseViewsSelector(_iidIndex: string) { +export function useDatabaseViewsSelector(_iidIndex: string, visibleViewIds?: string[]) { const database = useDatabase(); const views = database?.get(YjsDatabaseKey.views); @@ -46,7 +54,12 @@ export function useDatabaseViewsSelector(_iidIndex: string) { if (!views) return; const observerEvent = () => { - const viewsObj = views.toJSON(); + const viewsObj = views.toJSON() as Record< + string, + { + created_at: number; + } + >; const viewsSorted = Object.entries(viewsObj).sort((a, b) => { const [, viewA] = a; @@ -55,7 +68,13 @@ export function useDatabaseViewsSelector(_iidIndex: string) { return Number(viewB.created_at) - Number(viewA.created_at); }); - setViewIds(viewsSorted.map(([key]) => key)); + setViewIds( + viewsSorted + .map(([key]) => key) + .filter((id) => { + return !visibleViewIds || visibleViewIds.includes(id); + }) + ); }; observerEvent(); @@ -64,7 +83,7 @@ export function useDatabaseViewsSelector(_iidIndex: string) { return () => { views.unobserve(observerEvent); }; - }, [views]); + }, [views, visibleViewIds]); return { childViews, @@ -85,7 +104,8 @@ export function useFieldsSelector(visibilitys: FieldVisibility[] = defaultVisibl const fieldSettings = view?.get(YjsDatabaseKey.field_settings); const getColumns = () => { if (!fields || !fieldsOrder || !fieldSettings) return []; - const fieldIds = fieldsOrder.toJSON().map((item) => item.id) as string[]; + + const fieldIds = (fieldsOrder.toJSON() as { id: string }[]).map((item) => item.id); return fieldIds .map((fieldId) => { @@ -160,7 +180,7 @@ export function useFiltersSelector() { if (!filterOrders) return; const getFilters = () => { - return filterOrders.toJSON().map((item) => item.id); + return (filterOrders.toJSON() as { id: string }[]).map((item) => item.id); }; const observerEvent = () => setFilters(getFilters()); @@ -223,7 +243,7 @@ export function useSortsSelector() { if (!sortOrders) return; const getSorts = () => { - return sortOrders.toJSON().map((item) => item.id); + return (sortOrders.toJSON() as { id: string }[]).map((item) => item.id); }; const observerEvent = () => setSorts(getSorts()); @@ -287,12 +307,13 @@ export function useGroupsSelector() { useEffect(() => { if (!viewId) return; const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const groupOrders = view?.get(YjsDatabaseKey.groups); if (!groupOrders) return; const getGroups = () => { - return groupOrders.toJSON().map((item) => item.id); + return (groupOrders.toJSON() as { id: string }[]).map((item) => item.id); }; const observerEvent = () => setGroups(getGroups()); @@ -364,13 +385,13 @@ export function useRowsByGroup(groupId: string) { const fields = useDatabaseFields(); const [notFound, setNotFound] = useState(false); const [groupResult, setGroupResult] = useState>(new Map()); + const view = useDatabaseView(); + const layoutSetting = view?.get(YjsDatabaseKey.layout_settings)?.get('1'); useEffect(() => { if (!fieldId || !rowOrders || !rows) return; const onConditionsChange = () => { - if (rows.size < rowOrders?.length) return; - const newResult = new Map(); const field = fields.get(fieldId); @@ -399,7 +420,10 @@ export function useRowsByGroup(groupId: string) { }; }, [fieldId, fields, rowOrders, rows]); - const visibleColumns = columns.filter((column) => column.visible); + const visibleColumns = columns.filter((column) => { + if (column.id === fieldId) return !layoutSetting?.get(YjsDatabaseKey.hide_ungrouped_column); + return column.visible; + }); return { fieldId, @@ -450,18 +474,20 @@ export function useRowOrdersSelector() { }, [onConditionsChange, clock]); useEffect(() => { - const throttleChange = throttle(onConditionsChange, 200); + const throttleChange = debounce(onConditionsChange, 200); + view?.get(YjsDatabaseKey.row_orders)?.observeDeep(throttleChange); sorts?.observeDeep(throttleChange); filters?.observeDeep(throttleChange); fields?.observeDeep(throttleChange); return () => { + view?.get(YjsDatabaseKey.row_orders)?.unobserveDeep(throttleChange); sorts?.unobserveDeep(throttleChange); filters?.unobserveDeep(throttleChange); fields?.unobserveDeep(throttleChange); }; - }, [onConditionsChange, fields, filters, sorts]); + }, [onConditionsChange, view, fields, filters, sorts]); return rowOrders; } @@ -474,17 +500,10 @@ export function useRowDocMapSelector() { if (!rowMap) return; const observerEvent = () => setClock((prev) => prev + 1); - const rowIds = Array.from(rowMap?.keys() || []); - - rowMap.observe(observerEvent); - - const observers = rowIds.map((rowId) => { - return observeDeepRow(rowId, rowMap, observerEvent); - }); + rowMap.observeDeep(observerEvent); return () => { - rowMap.unobserve(observerEvent); - observers.forEach((observer) => observer()); + rowMap.unobserveDeep(observerEvent); }; }, [rowMap]); @@ -514,37 +533,20 @@ export function observeDeepRow( export function useRowDataSelector(rowId: string) { const rowMap = useRowDocMap(); - const rowSharedRoot = rowMap?.get(rowId)?.getMap(YjsEditorKey.data_section); + const rowDoc = rowMap?.get(rowId); + + const rowSharedRoot = rowDoc?.getMap(YjsEditorKey.data_section); const row = rowSharedRoot?.get(YjsEditorKey.database_row); - const [clock, setClock] = useState(0); - - useEffect(() => { - if (!rowMap) return; - const onChange = () => { - setClock((prev) => prev + 1); - }; - - const observer = observeDeepRow(rowId, rowMap, onChange); - - rowMap.observe(onChange); - - return () => { - rowMap.unobserve(onChange); - observer(); - }; - }, [rowId, rowMap]); - return { row, - clock, }; } export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: string }) { const { row } = useRowDataSelector(rowId); - const cell = row?.get(YjsDatabaseKey.cells)?.get(fieldId); + const [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined)); useEffect(() => { @@ -552,10 +554,10 @@ export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: st setCellValue(parseYDatabaseCellToCell(cell)); const observerEvent = () => setCellValue(parseYDatabaseCellToCell(cell)); - cell.observe(observerEvent); + cell.observeDeep(observerEvent); return () => { - cell.unobserve(observerEvent); + cell.unobserveDeep(observerEvent); }; }, [cell]); @@ -656,17 +658,20 @@ export function useCalendarLayoutSetting() { return setting; } +export function getPrimaryFieldId(database: YDatabase) { + const fields = database?.get(YjsDatabaseKey.fields); + + return Array.from(fields?.keys() || []).find((fieldId) => { + return fields?.get(fieldId)?.get(YjsDatabaseKey.is_primary); + }); +} + export function usePrimaryFieldId() { const database = useDatabase(); const [primaryFieldId, setPrimaryFieldId] = useState(null); useEffect(() => { - const fields = database?.get(YjsDatabaseKey.fields); - const primaryFieldId = Array.from(fields?.keys() || []).find((fieldId) => { - return fields?.get(fieldId)?.get(YjsDatabaseKey.is_primary); - }); - - setPrimaryFieldId(primaryFieldId || null); + setPrimaryFieldId(getPrimaryFieldId(database) || null); }, [database]); return primaryFieldId; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/sort.ts b/frontend/appflowy_web_app/src/application/database-yjs/sort.ts index cead275830..d572440c53 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/sort.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/sort.ts @@ -65,7 +65,7 @@ export function parseCellDataForSort(field: YDatabaseField, data: string | boole case FieldType.URL: return data ? data : '\uFFFF'; case FieldType.Number: - return data; + return data === 'string' && !isNaN(parseInt(data)) ? parseInt(data) : data; case FieldType.Checkbox: return data === 'Yes'; case FieldType.SingleSelect: diff --git a/frontend/appflowy_web_app/src/application/db/index.ts b/frontend/appflowy_web_app/src/application/db/index.ts index 6c7ae21264..9443352a87 100644 --- a/frontend/appflowy_web_app/src/application/db/index.ts +++ b/frontend/appflowy_web_app/src/application/db/index.ts @@ -21,7 +21,9 @@ const openedSet = new Set(); */ export async function openCollabDB(docName: string): Promise { const name = `${databasePrefix}_${docName}`; - const doc = new Y.Doc(); + const doc = new Y.Doc({ + guid: docName, + }); const provider = new IndexeddbPersistence(name, doc); @@ -56,3 +58,39 @@ export async function closeCollabDB(docName: string) { await provider.destroy(); } + +export async function clearData() { + try { + const databases = await indexedDB.databases(); + + databases.forEach((dbInfo) => { + const dbName = dbInfo.name as string; + const request = indexedDB.open(dbName); + + request.onsuccess = function (event) { + const db = (event.target as IDBOpenDBRequest).result; + + db.close(); + const deleteRequest = indexedDB.deleteDatabase(dbName); + + deleteRequest.onsuccess = function () { + console.log(`Database ${dbName} deleted successfully`); + }; + + deleteRequest.onerror = function (event) { + console.error(`Error deleting database ${dbName}`, event); + }; + + deleteRequest.onblocked = function () { + console.warn(`Delete operation blocked for database ${dbName}`); + }; + }; + + request.onerror = function (event) { + console.error(`Error opening database ${dbName}`, event); + }; + }); + } catch (e) { + console.error('Error listing databases:', e); + } +} diff --git a/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts b/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts index 8cc3573038..98efb3a6ea 100644 --- a/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts +++ b/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts @@ -8,6 +8,7 @@ export type ViewMeta = { ancestor_views: PublishViewInfo[]; visible_view_ids: string[]; + database_relations: Record; } & PublishViewInfo; export type ViewMetasTable = { diff --git a/frontend/appflowy_web_app/src/application/publish/context.tsx b/frontend/appflowy_web_app/src/application/publish/context.tsx index 8f99c28a42..6463f00b25 100644 --- a/frontend/appflowy_web_app/src/application/publish/context.tsx +++ b/frontend/appflowy_web_app/src/application/publish/context.tsx @@ -1,21 +1,20 @@ -import { YDoc } from '@/application/collab.type'; +import { GetViewRowsMap, LoadView, LoadViewMeta } from '@/application/collab.type'; import { db } from '@/application/db'; import { ViewMeta } from '@/application/db/tables/view_metas'; import { AFConfigContext } from '@/components/app/AppConfig'; import { useLiveQuery } from 'dexie-react-hooks'; -import { createContext, useCallback, useContext, useEffect, useRef } from 'react'; +import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import * as Y from 'yjs'; export interface PublishContextType { namespace: string; publishName: string; viewMeta?: ViewMeta; toView: (viewId: string) => Promise; - loadViewMeta: (viewId: string) => Promise; - getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map; destroy: () => void }>; + loadViewMeta: LoadViewMeta; + getViewRowsMap?: GetViewRowsMap; - loadView: (viewId: string) => Promise; + loadView: LoadView; } export const PublishContext = createContext(null); @@ -34,6 +33,39 @@ export const PublishProvider = ({ return db.view_metas.get(name); }, [namespace, publishName]); + const [subscribers, setSubscribers] = useState void>>(new Map()); + + useEffect(() => { + return () => { + setSubscribers(new Map()); + }; + }, []); + useEffect(() => { + db.view_metas.hook('creating', (primaryKey, obj) => { + const subscriber = subscribers.get(primaryKey); + + subscriber?.(obj); + + return obj; + }); + db.view_metas.hook('deleting', (primaryKey, obj) => { + const subscriber = subscribers.get(primaryKey); + + subscriber?.(obj); + + return; + }); + db.view_metas.hook('updating', (modifications, primaryKey, obj) => { + const subscriber = subscribers.get(primaryKey); + + subscriber?.({ + ...obj, + ...modifications, + }); + + return modifications; + }); + }, [subscribers]); const prevViewMeta = useRef(viewMeta); @@ -59,7 +91,7 @@ export const PublishProvider = ({ ); const loadViewMeta = useCallback( - async (viewId: string) => { + async (viewId: string, callback?: (meta: ViewMeta) => void) => { try { const info = await service?.getPublishInfo(viewId); @@ -69,13 +101,25 @@ export const PublishProvider = ({ const { namespace, publishName } = info; - const res = await service?.getPublishViewMeta(namespace, publishName); + const name = `${namespace}_${publishName}`; - if (!res) { - throw new Error('View meta has not been published yet'); + const meta = await service?.getPublishViewMeta(namespace, publishName); + + if (!meta) { + return Promise.reject(new Error('View meta has not been published yet')); } - return res; + callback?.(meta); + + if (callback) { + setSubscribers((prev) => { + prev.set(name, callback); + + return prev; + }); + } + + return meta; } catch (e) { return Promise.reject(e); } @@ -84,7 +128,7 @@ export const PublishProvider = ({ ); const getViewRowsMap = useCallback( - async (viewId: string, rowIds: string[]) => { + async (viewId: string, rowIds?: string[]) => { try { const info = await service?.getPublishInfo(viewId); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/__tests__/index.test.ts b/frontend/appflowy_web_app/src/application/services/js-services/__tests__/index.test.ts index 5dbf0b4f8c..be4341be4b 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/__tests__/index.test.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/__tests__/index.test.ts @@ -1,8 +1,9 @@ import { withTestingYDoc } from '@/application/slate-yjs/__tests__/withTestingYjsEditor'; +import * as Y from 'yjs'; import { AFClientService } from '../index'; import { fetchViewInfo } from '@/application/services/js-services/fetch'; import { expect, jest } from '@jest/globals'; -import { getBatchCollabs, getPublishView, getPublishViewMeta } from '@/application/services/js-services/cache'; +import { getPublishView, getPublishViewMeta } from '@/application/services/js-services/cache'; jest.mock('@/application/services/js-services/wasm/client_api', () => { return { @@ -69,18 +70,10 @@ describe('AFClientService', () => { it('should get view', async () => { const namespace = 'namespace'; const publishName = 'publishName'; + const rowDoc = new Y.Doc(); const mockResponse = { - data: [1, 2, 3], - meta: { - metadata: { - view: { - name: 'viewName', - view_id: 'view_id', - }, - child_views: [], - ancestor_views: [], - }, - }, + doc: withTestingYDoc('1'), + rowDocMap: rowDoc.getMap(), }; // @ts-ignore @@ -88,7 +81,7 @@ describe('AFClientService', () => { const result = await service.getPublishView(namespace, publishName); - expect(result).toEqual(mockResponse); + expect(result).toEqual(mockResponse.doc); }); it('should get view info', async () => { @@ -108,21 +101,4 @@ describe('AFClientService', () => { publishName: 'publishName', }); }); - - it('getPublishDatabaseViewRows', async () => { - const namespace = 'namespace'; - const publishName = 'publishName'; - const rowIds = ['1', '2', '3']; - const mockResponse = [withTestingYDoc('1'), withTestingYDoc('2'), withTestingYDoc('3')]; - - // @ts-ignore - (getBatchCollabs as jest.Mock).mockResolvedValue(mockResponse); - - const result = await service.getPublishDatabaseViewRows(namespace, publishName, rowIds); - - expect(result).toEqual({ - rows: expect.any(Object), - destroy: expect.any(Function), - }); - }); }); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/cache/__tests__/cache.test.ts b/frontend/appflowy_web_app/src/application/services/js-services/cache/__tests__/cache.test.ts index d49ddcab39..0b0fe59cf3 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/cache/__tests__/cache.test.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/cache/__tests__/cache.test.ts @@ -1,15 +1,9 @@ import { CollabType } from '@/application/collab.type'; import { withTestingYDoc } from '@/application/slate-yjs/__tests__/withTestingYjsEditor'; import { expect } from '@jest/globals'; -import { - collabTypeToDBType, - getPublishView, - getPublishViewMeta, - getBatchCollabs, -} from '@/application/services/js-services/cache'; +import { collabTypeToDBType, getPublishView, getPublishViewMeta } from '@/application/services/js-services/cache'; import { openCollabDB, db } from '@/application/db'; import { StrategyType } from '@/application/services/js-services/cache/types'; -import * as Y from 'yjs'; jest.mock('@/application/ydoc/apply', () => ({ applyYDoc: jest.fn(), @@ -59,6 +53,7 @@ describe('Cache functions', () => { describe('getPublishView', () => { it('should call fetcher when no cache found', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); mockFetcher.mockResolvedValue({ data: [1, 2, 3], meta: { metadata: { view: { id: '1' } } } }); (db.view_metas.get as jest.Mock).mockResolvedValue(undefined); await runTestWithStrategy(StrategyType.CACHE_FIRST); @@ -73,14 +68,14 @@ describe('Cache functions', () => { (db.view_metas.get as jest.Mock).mockResolvedValue({ view_id: '1' }); mockFetcher.mockResolvedValue({ data: [1, 2, 3], meta: { metadata: { view: { id: '1' } } } }); await runTestWithStrategy(StrategyType.CACHE_ONLY); - expect(openCollabDB).toBeCalledTimes(1); + expect(openCollabDB).toBeCalledTimes(2); await runTestWithStrategy(StrategyType.CACHE_FIRST); - expect(openCollabDB).toBeCalledTimes(2); + expect(openCollabDB).toBeCalledTimes(4); expect(mockFetcher).toBeCalledTimes(0); await runTestWithStrategy(StrategyType.CACHE_AND_NETWORK); - expect(openCollabDB).toBeCalledTimes(3); + expect(openCollabDB).toBeCalledTimes(6); expect(mockFetcher).toBeCalledTimes(1); }); }); @@ -116,20 +111,6 @@ describe('Cache functions', () => { expect(mockFetcher).toBeCalledTimes(1); }); }); - - describe('getBatchCollabs', () => { - it('should return empty array when no cache found', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(new Y.Doc()); - await expect(getBatchCollabs(['1', '2', '3'])).rejects.toThrow('No cache found'); - }); - - it('should return collabs when cache found', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); - (db.view_metas.get as jest.Mock).mockResolvedValue({ view_id: '1' }); - const collabs = await getBatchCollabs(['1', '2', '3']); - expect(collabs).toEqual([normalDoc, normalDoc, normalDoc]); - }); - }); }); describe('collabTypeToDBType', () => { diff --git a/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts index 51ea75f511..d33bf9a939 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts @@ -1,7 +1,10 @@ import { CollabType, + DatabaseId, PublishViewInfo, PublishViewMetaData, + RowId, + ViewId, YDoc, YjsEditorKey, YSharedRoot, @@ -9,6 +12,8 @@ import { import { applyYDoc } from '@/application/ydoc/apply'; import { closeCollabDB, db, openCollabDB } from '@/application/db'; import { Fetcher, StrategyType } from '@/application/services/js-services/cache/types'; +import { IndexeddbPersistence } from 'y-indexeddb'; +import * as Y from 'yjs'; export function collabTypeToDBType(type: CollabType) { switch (type) { @@ -110,8 +115,9 @@ export async function getPublishViewMeta< export async function getPublishView< T extends { data: number[]; - rows?: Record; - visibleViewIds?: string[]; + rows?: Record; + visibleViewIds?: ViewId[]; + relations?: Record; meta: { view: PublishViewInfo; child_views: PublishViewInfo[]; @@ -131,6 +137,22 @@ export async function getPublishView< ) { const name = `${namespace}_${publishName}`; const doc = await openCollabDB(name); + const rowMapDoc = (await openCollabDB(`${name}_rows`)) as Y.Doc; + + const subdocs = Array.from(rowMapDoc.getSubdocs()); + + for (const subdoc of subdocs) { + const promise = new Promise((resolve) => { + const persistence = new IndexeddbPersistence(subdoc.guid, subdoc); + + persistence.on('synced', () => { + resolve(true); + }); + }); + + await promise; + } + const exist = (await hasViewMetaCache(name)) && hasCollabCache(doc); switch (strategy) { @@ -144,7 +166,7 @@ export async function getPublishView< case StrategyType.CACHE_FIRST: { if (!exist) { - await revalidatePublishView(name, fetcher, doc); + await revalidatePublishView(name, fetcher, doc, rowMapDoc); } break; @@ -152,21 +174,21 @@ export async function getPublishView< case StrategyType.CACHE_AND_NETWORK: { if (!exist) { - await revalidatePublishView(name, fetcher, doc); + await revalidatePublishView(name, fetcher, doc, rowMapDoc); } else { - void revalidatePublishView(name, fetcher, doc); + void revalidatePublishView(name, fetcher, doc, rowMapDoc); } break; } default: { - await revalidatePublishView(name, fetcher, doc); + await revalidatePublishView(name, fetcher, doc, rowMapDoc); break; } } - return doc; + return { doc, rowMapDoc }; } export async function revalidatePublishViewMeta< @@ -187,6 +209,7 @@ export async function revalidatePublishViewMeta< child_views: child_views, ancestor_views: ancestor_views, visible_view_ids: dbView?.visible_view_ids ?? [], + database_relations: dbView?.database_relations ?? {}, }, name ); @@ -197,12 +220,13 @@ export async function revalidatePublishViewMeta< export async function revalidatePublishView< T extends { data: number[]; - rows?: Record; - visibleViewIds?: string[]; + rows?: Record; + visibleViewIds?: ViewId[]; + relations?: Record; meta: PublishViewMetaData; } ->(name: string, fetcher: Fetcher, collab: YDoc) { - const { data, meta, rows, visibleViewIds = [] } = await fetcher(); +>(name: string, fetcher: Fetcher, collab: YDoc, rowMapDoc: Y.Doc) { + const { data, meta, rows, visibleViewIds = [], relations = {} } = await fetcher(); await db.view_metas.put( { @@ -211,15 +235,23 @@ export async function revalidatePublishView< child_views: meta.child_views, ancestor_views: meta.ancestor_views, visible_view_ids: visibleViewIds, + database_relations: relations, }, name ); if (rows) { for (const [key, value] of Object.entries(rows)) { - const row = await openCollabDB(`${name}_${key}`); + const subdoc = new Y.Doc({ + guid: key, + }); + const persistence = new IndexeddbPersistence(subdoc.guid, subdoc); - applyYDoc(row, new Uint8Array(value)); + persistence.on('synced', () => { + applyYDoc(subdoc, new Uint8Array(value)); + rowMapDoc.getMap().delete(subdoc.guid); + rowMapDoc.getMap().set(subdoc.guid, subdoc); + }); } } @@ -228,27 +260,6 @@ export async function revalidatePublishView< applyYDoc(collab, state); } -export async function getBatchCollabs(names: string[]) { - const getRowDoc = async (name: string) => { - const doc = await openCollabDB(name); - const exist = hasCollabCache(doc); - - if (!exist) { - return Promise.reject(new Error('No cache found')); - } - - return doc; - }; - - const collabs = await Promise.all( - names.map((name) => { - return getRowDoc(name); - }) - ); - - return collabs; -} - export async function deleteViewMeta(name: string) { await db.view_metas.delete(name); } @@ -257,4 +268,15 @@ export async function deleteView(name: string) { console.log('deleteView', name); await deleteViewMeta(name); await closeCollabDB(name); + const rowMapDoc = (await openCollabDB(`${name}_rows`)) as Y.Doc; + + const subdocs = Array.from(rowMapDoc.getSubdocs()); + + for (const subdoc of subdocs) { + const persistence = new IndexeddbPersistence(subdoc.guid, subdoc); + + await persistence.destroy(); + } + + await closeCollabDB(`${name}_rows`); } 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 21d739317c..42675d88dc 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,6 @@ import { YDoc } from '@/application/collab.type'; import { deleteView, - getBatchCollabs, getPublishView, getPublishViewMeta, hasViewMetaCache, @@ -52,6 +51,9 @@ export class AFClientService implements AFService { } async getPublishViewMeta(namespace: string, publishName: string) { + const name = `${namespace}_${publishName}`; + + const isLoaded = this.publishViewLoaded.has(name); const viewMeta = await getPublishViewMeta( () => { return fetchPublishViewMeta(namespace, publishName); @@ -60,7 +62,7 @@ export class AFClientService implements AFService { namespace, publishName, }, - StrategyType.CACHE_AND_NETWORK + isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK ); if (!viewMeta) { @@ -75,11 +77,12 @@ export class AFClientService implements AFService { const isLoaded = this.publishViewLoaded.has(name); - const doc = await getPublishView( + const { doc, rowMapDoc } = await getPublishView( async () => { try { return await fetchPublishView(namespace, publishName); } catch (e) { + console.error(e); void (async () => { if (await hasViewMetaCache(name)) { this.publishViewLoaded.delete(name); @@ -101,39 +104,30 @@ export class AFClientService implements AFService { this.publishViewLoaded.add(name); } + this.cacheDatabaseRowDocMap.set(name, rowMapDoc); + return doc; } - async getPublishDatabaseViewRows(namespace: string, publishName: string, rowIds: string[]) { + async getPublishDatabaseViewRows(namespace: string, publishName: string) { const name = `${namespace}_${publishName}`; - if (!this.publishViewLoaded.has(name)) { + if (!this.publishViewLoaded.has(name) || !this.cacheDatabaseRowDocMap.has(name)) { await this.getPublishView(namespace, publishName); } - const rootRowsDoc = - this.cacheDatabaseRowDocMap.get(name) ?? - new Y.Doc({ - guid: name, - }); + const rootRowsDoc = this.cacheDatabaseRowDocMap.get(name); - if (!this.cacheDatabaseRowDocMap.has(name)) { - this.cacheDatabaseRowDocMap.set(name, rootRowsDoc); + if (!rootRowsDoc) { + return Promise.reject(new Error('Root rows doc not found')); } const rowsFolder: Y.Map = rootRowsDoc.getMap(); - const docs = await getBatchCollabs(rowIds.map((id) => `${name}_${id}`)); - docs.forEach((doc, index) => { - rowsFolder.set(rowIds[index], doc); - }); - - console.log('getPublishDatabaseViewRows', docs); return { rows: rowsFolder, destroy: () => { this.cacheDatabaseRowDocMap.delete(name); - rootRowsDoc.destroy(); }, }; } 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 b3865b7671..852559c3aa 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,7 +1,7 @@ import { getToken, invalidToken, isTokenValid, refreshToken } from '@/application/session/token'; import { ClientAPI } from '@appflowyinc/client-api-wasm'; import { AFCloudConfig } from '@/application/services/services.type'; -import { PublishViewMetaData, ViewLayout } from '@/application/collab.type'; +import { DatabaseId, PublishViewMetaData, RowId, ViewId, ViewLayout } from '@/application/collab.type'; let client: ClientAPI; @@ -52,19 +52,22 @@ export async function getPublishView(publishNamespace: string, publishName: stri try { const decoder = new TextDecoder('utf-8'); + const jsonStr = decoder.decode(new Uint8Array(data.data)); + const res = JSON.parse(jsonStr) as { database_collab: number[]; - database_row_collabs: Record; + database_row_collabs: Record; database_row_document_collabs: Record; - visible_database_view_ids: string[]; + visible_database_view_ids: ViewId[]; + database_relations: Record; }; - console.log('getPublishView', res); return { data: res.database_collab, rows: res.database_row_collabs, visibleViewIds: res.visible_database_view_ids, + relations: res.database_relations, meta, }; } catch (e) { 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 1eea995246..919cbf5306 100644 --- a/frontend/appflowy_web_app/src/application/services/services.type.ts +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -22,7 +22,7 @@ export interface PublishService { getPublishDatabaseViewRows: ( namespace: string, publishName: string, - rowIds: string[] + rowIds?: string[] ) => Promise<{ rows: Y.Map; destroy: () => void; 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 36f178cc57..e5b970e74b 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 @@ -1,5 +1,7 @@ +import { YDoc } from '@/application/collab.type'; import { AFService } from '@/application/services/services.type'; import { nanoid } from 'nanoid'; +import { YMap } from 'yjs/dist/src/types/YMap'; export class AFClientService implements AFService { private deviceId: string = nanoid(8); @@ -18,10 +20,6 @@ export class AFClientService implements AFService { return Promise.reject('Method not implemented'); } - async getPublishDatabaseViewRows(_namespace: string, _publishName: string, _rowIds: string[]) { - return Promise.reject('Method not implemented'); - } - getClientId(): string { return ''; } @@ -45,4 +43,14 @@ export class AFClientService implements AFService { signInMagicLink(_params: { email: string; redirectTo: string }): Promise { return Promise.resolve(undefined); } + + getPublishDatabaseViewRows( + _namespace: string, + _publishName: string + ): Promise<{ + rows: YMap; + destroy: () => void; + }> { + return Promise.reject('Method not implemented'); + } } 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 3b9f527087..7ce5253e9f 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 @@ -13,7 +13,7 @@ export interface YjsEditor extends Editor { connect: () => void; disconnect: () => void; sharedRoot: YSharedRoot; - applyRemoteEvents: (events: Array>, transaction: Transaction) => void; + applyRemoteEvents: (events: Array, transaction: Transaction) => void; flushLocalChanges: () => void; storeLocalChange: (op: Operation) => void; } @@ -36,7 +36,7 @@ export const YjsEditor = { editor.disconnect(); }, - applyRemoteEvents(editor: YjsEditor, events: Array>, transaction: Transaction): void { + applyRemoteEvents(editor: YjsEditor, events: Array, transaction: Transaction): void { editor.applyRemoteEvents(events, transaction); }, @@ -90,7 +90,7 @@ export function withYjs( apply(op); }; - e.applyRemoteEvents = (_events: Array>, _: Transaction) => { + e.applyRemoteEvents = (_events: Array, _: Transaction) => { // Flush local changes to ensure all local changes are applied before processing remote events YjsEditor.flushLocalChanges(e); // Replace the apply function to avoid storing remote changes as local changes @@ -103,7 +103,7 @@ export function withYjs( e.apply = applyIntercept; }; - const handleYEvents = (events: Array>, transaction: Transaction) => { + const handleYEvents = (events: Array, transaction: Transaction) => { if (transaction.origin === CollabOrigin.Remote) { YjsEditor.applyRemoteEvents(e, events, transaction); } diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts index 954ad3518e..47108f4cab 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts @@ -25,10 +25,16 @@ export function yDataToSlateContent({ rootId: string; }): Element | undefined { function traverse(id: string) { - const block = blocks.get(id).toJSON() as BlockJson; + const block = blocks.get(id)?.toJSON() as BlockJson; + + if (!block) { + console.error('Block not found', id); + return; + } + const childrenId = block.children as string; - const children = (childrenMap.get(childrenId)?.toJSON() ?? []).map(traverse) as (Element | Text)[]; + const children = (childrenMap.get(childrenId)?.toJSON() ?? []).map(traverse).filter(Boolean) as (Element | Text)[]; const slateNode = blockToSlateNode(block); diff --git a/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts b/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts index b19cb43328..1a0eb39c78 100644 --- a/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts +++ b/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts @@ -11,7 +11,12 @@ export function applyYDoc(doc: Y.Doc, state: Uint8Array) { Y.transact( doc, () => { - Y.applyUpdate(doc, state); + try { + Y.applyUpdate(doc, state); + } catch (e) { + console.error('Error applying', doc, e); + throw e; + } }, CollabOrigin.Remote ); diff --git a/frontend/appflowy_web_app/src/assets/ai_indicator.svg b/frontend/appflowy_web_app/src/assets/ai_indicator.svg new file mode 100644 index 0000000000..690c01ac0b --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/ai_indicator.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/ai_summary.svg b/frontend/appflowy_web_app/src/assets/ai_summary.svg new file mode 100644 index 0000000000..37c94e5e28 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/ai_summary.svg @@ -0,0 +1,4 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/ai_translate.svg b/frontend/appflowy_web_app/src/assets/ai_translate.svg new file mode 100644 index 0000000000..237fc04194 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/ai_translate.svg @@ -0,0 +1,4 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/checkbox.svg b/frontend/appflowy_web_app/src/assets/checkbox.svg new file mode 100644 index 0000000000..37f52c47ed --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/checkbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/checklist.svg b/frontend/appflowy_web_app/src/assets/checklist.svg new file mode 100644 index 0000000000..3a88d236a1 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/checklist.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/created_at.svg b/frontend/appflowy_web_app/src/assets/created_at.svg new file mode 100644 index 0000000000..4469e7f0e3 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/created_at.svg @@ -0,0 +1,12 @@ + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/date.svg b/frontend/appflowy_web_app/src/assets/date.svg new file mode 100644 index 0000000000..78243f1e75 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/date.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/last_modified.svg b/frontend/appflowy_web_app/src/assets/last_modified.svg new file mode 100644 index 0000000000..22861c86e3 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/last_modified.svg @@ -0,0 +1,4 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/link.svg b/frontend/appflowy_web_app/src/assets/link.svg new file mode 100644 index 0000000000..5fbcc8d787 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/multiselect.svg b/frontend/appflowy_web_app/src/assets/multiselect.svg new file mode 100644 index 0000000000..97a2e9c434 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/multiselect.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/number.svg b/frontend/appflowy_web_app/src/assets/number.svg new file mode 100644 index 0000000000..9d8b98d10d --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/number.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/relation.svg b/frontend/appflowy_web_app/src/assets/relation.svg new file mode 100644 index 0000000000..f82a41d226 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/relation.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/single_select.svg b/frontend/appflowy_web_app/src/assets/single_select.svg new file mode 100644 index 0000000000..8ccbc9a2e3 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/single_select.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/text.svg b/frontend/appflowy_web_app/src/assets/text.svg new file mode 100644 index 0000000000..7befa5080f --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/text.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/url.svg b/frontend/appflowy_web_app/src/assets/url.svg new file mode 100644 index 0000000000..f00f5c7aa2 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/url.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx b/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx index 911ccff808..bdb17db832 100644 --- a/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx @@ -16,9 +16,10 @@ export const AFScroller = React.forwardRef( { if (!el) return; - + const scrollEl = el.container?.firstChild as HTMLElement; if (!scrollEl) return; diff --git a/frontend/appflowy_web_app/src/components/app/App.tsx b/frontend/appflowy_web_app/src/components/app/App.tsx index e029a9f207..6ead293033 100644 --- a/frontend/appflowy_web_app/src/components/app/App.tsx +++ b/frontend/appflowy_web_app/src/components/app/App.tsx @@ -1,6 +1,7 @@ import { AUTH_CALLBACK_PATH } from '@/application/session/sign_in'; import NotFound from '@/components/error/NotFound'; import LoginAuth from '@/components/login/LoginAuth'; +import AfterPaymentPage from '@/pages/AfterPaymentPage'; import LoginPage from '@/pages/LoginPage'; import PublishPage from '@/pages/PublishPage'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; @@ -14,6 +15,7 @@ const AppMain = withAppWrapper(() => { } /> } /> } /> + } /> } /> ); diff --git a/frontend/appflowy_web_app/src/components/app/AppConfig.tsx b/frontend/appflowy_web_app/src/components/app/AppConfig.tsx index 8d5c6c2b64..b4ef0e60d4 100644 --- a/frontend/appflowy_web_app/src/components/app/AppConfig.tsx +++ b/frontend/appflowy_web_app/src/components/app/AppConfig.tsx @@ -1,3 +1,4 @@ +import { clearData } from '@/application/db'; import { EventType, on } from '@/application/session'; import { isTokenValid } from '@/application/session/token'; import { useAppLanguage } from '@/components/app/useAppLanguage'; @@ -88,6 +89,23 @@ function AppConfig({ children }: { children: React.ReactNode }) { }; }, [closeSnackbar, enqueueSnackbar]); + useEffect(() => { + const handleClearData = (e: KeyboardEvent) => { + if (e.key.toLowerCase() === 'r' && (e.ctrlKey || e.metaKey) && e.shiftKey) { + e.stopPropagation(); + e.preventDefault(); + void clearData().then(() => { + window.location.reload(); + }); + } + }; + + window.addEventListener('keydown', handleClearData); + return () => { + window.removeEventListener('keydown', handleClearData); + }; + }); + return ( Promise<{ rows: Y.Map; destroy: () => void }>; - loadView?: (viewId: string) => Promise; + getViewRowsMap?: GetViewRowsMap; + loadView?: LoadView; navigateToView?: (viewId: string) => Promise; - loadViewMeta?: (viewId: string) => Promise; + loadViewMeta?: LoadViewMeta; + viewId: string; + iidName: string; + rowId?: string; + onChangeView: (viewId: string) => void; + onOpenRow?: (rowId: string) => void; + visibleViewIds: string[]; + iidIndex: string; } -function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView, ...viewMeta }: Database2Props) { - const [search, setSearch] = useSearchParams(); +function Database({ + doc, + getViewRowsMap, + navigateToView, + loadViewMeta, + loadView, + viewId, + iidIndex, + iidName, + visibleViewIds, + rowId, + onChangeView, + onOpenRow, +}: Database2Props) { + const database = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase; - const viewId = search.get('v') || viewMeta.viewId; - - const rowIds = useMemo(() => { - if (!viewId) return []; - const database = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase; - const rows = database.get(YjsDatabaseKey.views).get(viewId).get(YjsDatabaseKey.row_orders); - - return rows.toJSON().map((row) => row.id); - }, [doc, viewId]); - - const iidIndex = useMemo(() => { - const database = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase; - - return database.get(YjsDatabaseKey.metas).get(YjsDatabaseKey.iid); - }, [doc]); + const view = database.get(YjsDatabaseKey.views).get(iidIndex); + const rowOrders = view.get(YjsDatabaseKey.row_orders); const [rowDocMap, setRowDocMap] = useState | null>(null); + const handleUpdateRowDocMap = useCallback(async () => { + if (!getViewRowsMap || !iidIndex) return; + + const { rows, destroy } = await getViewRowsMap(iidIndex); + + setRowDocMap(rows); + return destroy; + }, [getViewRowsMap, iidIndex]); + useEffect(() => { - if (!getViewRowsMap || !rowIds.length || !iidIndex) return; + void handleUpdateRowDocMap(); - void (async () => { - const { rows, destroy } = await getViewRowsMap(iidIndex, rowIds); - - setRowDocMap(rows); - return destroy; - })(); - }, [getViewRowsMap, rowIds, iidIndex]); - - const rowId = search.get('r'); - - const handleChangeView = useCallback( - (viewId: string) => { - setSearch({ v: viewId }); - }, - [setSearch] - ); - - const handleNavigateToRow = useCallback( - (rowId: string) => { - setSearch({ r: rowId }); - }, - [setSearch] - ); + rowOrders?.observe(handleUpdateRowDocMap); + return () => { + rowOrders?.unobserve(handleUpdateRowDocMap); + }; + }, [handleUpdateRowDocMap, rowOrders]); if (!rowDocMap || !viewId) { return null; } return ( -
+
}> {rowId ? ( ) : ( -
- {viewMeta && } - -
- -
+
+
)} diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx index f9682e6b5f..48bc16e44d 100644 --- a/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx +++ b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx @@ -16,13 +16,17 @@ function DatabaseViews({ viewId, iidIndex, viewName, + visibleViewIds, + hideConditions = false, }: { onChangeView: (viewId: string) => void; viewId: string; iidIndex: string; viewName?: string; + visibleViewIds?: string[]; + hideConditions?: boolean; }) { - const { childViews, viewIds } = useDatabaseViewsSelector(iidIndex); + const { childViews, viewIds } = useDatabaseViewsSelector(iidIndex, visibleViewIds); const value = useMemo(() => { return Math.max( @@ -40,10 +44,12 @@ function DatabaseViews({ return childViews[value]; }, [childViews, value]); - const view = useMemo(() => { + const layout = useMemo(() => { if (!activeView) return null; - const layout = Number(activeView.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; + return Number(activeView.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; + }, [activeView]); + const view = useMemo(() => { switch (layout) { case DatabaseViewLayout.Grid: return ; @@ -52,7 +58,7 @@ function DatabaseViews({ case DatabaseViewLayout.Calendar: return ; } - }, [activeView]); + }, [layout]); return ( <> @@ -68,8 +74,9 @@ function DatabaseViews({ selectedViewId={viewId} setSelectedViewId={onChangeView} viewIds={viewIds} + hideConditions={hideConditions} /> - + {layout === DatabaseViewLayout.Calendar || hideConditions ? null : }
}> diff --git a/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseRow.cy.tsx b/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseRow.cy.tsx index bd6e6c5125..44b07d08aa 100644 --- a/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseRow.cy.tsx +++ b/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseRow.cy.tsx @@ -74,6 +74,7 @@ function TestDatabaseRow({ }) { return ( diff --git a/frontend/appflowy_web_app/src/components/database/board/Board.tsx b/frontend/appflowy_web_app/src/components/database/board/Board.tsx index 73c0f95398..a92ae8c705 100644 --- a/frontend/appflowy_web_app/src/components/database/board/Board.tsx +++ b/frontend/appflowy_web_app/src/components/database/board/Board.tsx @@ -7,6 +7,7 @@ export function Board() { const database = useDatabase(); const groups = useGroupsSelector(); + console.log('groups', database); if (!database) { return (
diff --git a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx index 5a25cd6e49..6a710ab655 100644 --- a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx +++ b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx @@ -8,7 +8,7 @@ export function Calendar() { const { dayPropGetter, localizer, formats, events, emptyEvents } = useCalendarSetup(); return ( -
+
, diff --git a/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss index 5829d4e1cc..bbba46f82e 100644 --- a/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss +++ b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss @@ -33,13 +33,16 @@ $today-highlight-bg: transparent; .rbc-month-row { border: 1px solid var(--line-divider); border-top: none; - + min-width: 700px; } @include mixin.scrollbar-style; } +.rbc-day-bg + .rbc-day-bg { + border-left-color: var(--line-divider); +} .rbc-month-header { height: 40px; @@ -47,6 +50,7 @@ $today-highlight-bg: transparent; top: 0; background: var(--bg-body); z-index: 50; + min-width: 700px; .rbc-header { border: none; @@ -72,6 +76,8 @@ $today-highlight-bg: transparent; flex: 0 0 0 !important; min-height: 97px !important; height: fit-content; + table-layout: fixed; + width: 100%; } .event-properties { diff --git a/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx index df1efbf840..dd195ca81f 100644 --- a/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx @@ -43,7 +43,7 @@ export const Card = memo(({ groupFieldId, rowId, onResize, isDragging }: CardPro style={{ minHeight: '38px', }} - className='relative flex cursor-pointer flex-col rounded-lg border border-line-border p-3 text-xs shadow-sm hover:bg-fill-list-active hover:shadow' + className='relative flex flex-col gap-2 rounded-lg border border-line-border p-3 text-xs shadow-sm' > {showFields.map((field, index) => { return ; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx b/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx index 28982b3bfc..831aa24f34 100644 --- a/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx @@ -1,6 +1,5 @@ import { Row } from '@/application/database-yjs'; import { AFScroller } from '@/components/_shared/scroller'; -import { Tag } from '@/components/_shared/tag'; import ListItem from '@/components/database/components/board/column/ListItem'; import { useRenderColumn } from '@/components/database/components/board/column/useRenderColumn'; import { useMeasureHeight } from '@/components/database/components/cell/useMeasure'; @@ -72,9 +71,7 @@ export const Column = memo( return (
-
- -
+
{header}
diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts b/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts deleted file mode 100644 index c845d4b5a3..0000000000 --- a/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { YjsDatabaseKey } from '@/application/collab.type'; -import { FieldType, parseSelectOptionTypeOptions, useFieldSelector } from '@/application/database-yjs'; -import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; -import { useMemo } from 'react'; - -export function useRenderColumn(id: string, fieldId: string) { - const { field } = useFieldSelector(fieldId); - const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; - const fieldName = field?.get(YjsDatabaseKey.name) || ''; - const header = useMemo(() => { - if (!field) return null; - switch (fieldType) { - case FieldType.SingleSelect: - case FieldType.MultiSelect: { - const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option.id === id); - - return { - name: option?.name || `No ${fieldName}`, - color: option?.color ? SelectOptionColorMap[option?.color] : 'transparent', - }; - } - - default: - return null; - } - }, [field, fieldName, fieldType, id]); - - return { - header, - }; -} diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.tsx b/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.tsx new file mode 100644 index 0000000000..7572bbb8cd --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.tsx @@ -0,0 +1,51 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, parseSelectOptionTypeOptions, useFieldSelector } from '@/application/database-yjs'; +import { Tag } from '@/components/_shared/tag'; +import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; + +export function useRenderColumn(id: string, fieldId: string) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + const fieldName = field?.get(YjsDatabaseKey.name) || ''; + const { t } = useTranslation(); + const header = useMemo(() => { + if (!field) return null; + if (fieldType === FieldType.Checkbox) + return ( +
+ {id === 'Yes' ? ( + <> + + {t('button.yes')} + + ) : ( + <> + {' '} + + {t('button.no')} + + )} +
+ ); + if ([FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) { + const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option.id === id); + + return ( + + ); + } + + return null; + }, [field, fieldType, id, fieldName, t]); + + return { + header, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx index 5331f4f8c2..77ab998ff3 100644 --- a/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx @@ -15,7 +15,7 @@ export const Group = ({ groupId }: GroupProps) => { if (notFound) { return ( -
+
{t('board.noGroup')}
{t('board.noGroupDesc')}
@@ -24,7 +24,7 @@ export const Group = ({ groupId }: GroupProps) => { if (columns.length === 0 || !fieldId) return null; return ( - +
{columns.map((data) => ( diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx index f2dc324409..d047ee4d7c 100644 --- a/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx @@ -2,14 +2,13 @@ import { CalendarEvent, useFieldsSelector } from '@/application/database-yjs'; import { RichTooltip } from '@/components/_shared/popover'; import EventPaper from '@/components/database/components/calendar/event/EventPaper'; import CardField from '@/components/database/components/field/CardField'; -import React, { useMemo } from 'react'; +import React from 'react'; import { EventWrapperProps } from 'react-big-calendar'; export function Event({ event }: EventWrapperProps) { const { id } = event; - const [rowId, fieldId] = id.split(':'); - const fields = useFieldsSelector(); - const showFields = useMemo(() => fields.filter((field) => field.fieldId !== fieldId), [fields, fieldId]); + const [rowId] = id.split(':'); + const showFields = useFieldsSelector(); // const navigateToRow = useNavigateToRow(); const [open, setOpen] = React.useState(false); @@ -26,21 +25,11 @@ export function Event({ event }: EventWrapperProps) { } }} className={ - 'flex min-h-[24px] cursor-pointer flex-col gap-2 rounded-md border border-line-border bg-bg-body p-2 text-xs shadow-sm hover:bg-fill-list-active hover:shadow' + 'flex min-h-[24px] cursor-pointer flex-col gap-2 rounded-md border border-line-border bg-bg-body p-2 text-xs text-xs shadow-sm hover:bg-fill-list-active hover:shadow' } > {showFields.map((field) => { - return ( -
- -
- ); + return ; })}
diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx index b6238185eb..79fb0092ea 100644 --- a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx @@ -1,7 +1,6 @@ import { CalendarEvent } from '@/application/database-yjs'; import { RichTooltip } from '@/components/_shared/popover'; import NoDateRow from '@/components/database/components/calendar/toolbar/NoDateRow'; -import Button from '@mui/material/Button'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,7 +10,7 @@ function NoDate({ emptyEvents }: { emptyEvents: CalendarEvent[] }) { const content = useMemo(() => { return (
-
{t('calendar.settings.clickToOpen')}
+ {/*
{t('calendar.settings.clickToOpen')}
*/} {emptyEvents.map((event) => { const rowId = event.id.split(':')[0]; @@ -19,7 +18,7 @@ function NoDate({ emptyEvents }: { emptyEvents: CalendarEvent[] }) { })}
); - }, [emptyEvents, t]); + }, [emptyEvents]); return ( - + ); } diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx index 5e2eaa61d2..7586574bb0 100644 --- a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx @@ -1,10 +1,10 @@ -import { useCellSelector, useNavigateToRow, usePrimaryFieldId } from '@/application/database-yjs'; +import { useCellSelector, usePrimaryFieldId } from '@/application/database-yjs'; import { Cell } from '@/components/database/components/cell'; import React from 'react'; import { useTranslation } from 'react-i18next'; function NoDateRow({ rowId }: { rowId: string }) { - const navigateToRow = useNavigateToRow(); + // const navigateToRow = useNavigateToRow(); const primaryFieldId = usePrimaryFieldId(); const cell = useCellSelector({ rowId, @@ -18,15 +18,15 @@ function NoDateRow({ rowId }: { rowId: string }) { return (
{ - navigateToRow?.(rowId); - }} + // onClick={() => { + // navigateToRow?.(rowId); + // }} className={'w-full hover:text-fill-default'} > onNavigate('TODAY')} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx index 3b480f946a..9ffe88ee3f 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx @@ -6,10 +6,10 @@ import { CellProps, CheckboxCell as CheckboxCellType } from '@/application/datab export function CheckboxCell({ cell, style }: CellProps) { const checked = cell?.data; - if (cell?.fieldType !== FieldType.Checkbox) return null; + if (cell && cell?.fieldType !== FieldType.Checkbox) return null; return ( -
+
{checked ? : }
); diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx index e3c927a607..c7dfcd0d78 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx @@ -1,6 +1,7 @@ import { FieldType, parseChecklistData } from '@/application/database-yjs'; import { CellProps, ChecklistCell as ChecklistCellType } from '@/application/database-yjs/cell.type'; import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel'; +import { isNaN } from 'lodash-es'; import React, { useMemo } from 'react'; export function ChecklistCell({ cell, style, placeholder }: CellProps) { @@ -19,8 +20,10 @@ export function ChecklistCell({ cell, style, placeholder }: CellProps ) : null; + + if (isNaN(data?.percentage)) return null; return ( -
+
); diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx index 56e7027567..16830ea201 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx @@ -34,7 +34,7 @@ export function RowCreateModifiedTime({ const time = useMemo(() => { if (!value) return null; - return getDateTimeStr(value, false); + return getDateTimeStr(value, true); }, [value, getDateTimeStr]); if (!time) return null; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx index ca3ab41957..8585b47a20 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx @@ -35,8 +35,8 @@ export function DateTimeCell({ cell, fieldId, style, placeholder }: CellProps - {hasReminder && } {dateStr} + {hasReminder && }
); } diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx index 4217c880c4..4b6660cfe3 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx @@ -25,6 +25,9 @@ export function NumberCell({ cell, fieldId, style, placeholder }: CellProps) { const { rowId } = props; @@ -40,19 +40,19 @@ export function PrimaryCell(props: CellProps) { }; }, [rowId]); - const isMobile = useMemo(() => { - return getPlatform().isMobile; - }, []); - - const navigateToRow = useNavigateToRow(); + // const isMobile = useMemo(() => { + // return getPlatform().isMobile; + // }, []); + // + // const navigateToRow = useNavigateToRow(); return (
{ - if (isMobile) { - navigateToRow?.(rowId); - } - }} + // onClick={() => { + // if (isMobile) { + // navigateToRow?.(rowId); + // } + // }} className={'primary-cell relative flex min-h-full w-full items-center gap-2'} > {icon &&
{icon}
} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx index 7086d70a1b..8695696273 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx @@ -1,47 +1,124 @@ -import { YjsDatabaseKey } from '@/application/collab.type'; -import { DatabaseContext, DatabaseContextState, useDatabase, useNavigateToRow } from '@/application/database-yjs'; +import { YDatabase, YDoc, YjsEditorKey } from '@/application/collab.type'; +import { + DatabaseContext, + DatabaseContextState, + getPrimaryFieldId, + parseRelationTypeOption, + useFieldSelector, +} from '@/application/database-yjs'; import { RelationCell, RelationCellData } from '@/application/database-yjs/cell.type'; +import { ViewMeta } from '@/application/db/tables/view_metas'; import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue'; -import React, { useContext, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; + +function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) { + const viewId = useContext(DatabaseContext)?.iidIndex; + const { field } = useFieldSelector(fieldId); + const relatedDatabaseId = field ? parseRelationTypeOption(field).database_id : null; -function RelationItems({ style, cell }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) { - const database = useDatabase(); - const viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString(); - const rowIds = useMemo(() => { - return (cell.data?.toJSON() as RelationCellData) ?? []; - }, [cell.data]); const getViewRowsMap = useContext(DatabaseContext)?.getViewRowsMap; + const loadViewMeta = useContext(DatabaseContext)?.loadViewMeta; + const loadView = useContext(DatabaseContext)?.loadView; + const [noAccess, setNoAccess] = useState(false); + const [relations, setRelations] = useState | null>(); const [rows, setRows] = useState(); + const [relatedFieldId, setRelatedFieldId] = useState(); + const relatedViewId = relatedDatabaseId ? relations?.[relatedDatabaseId] : null; - const navigateToRow = useNavigateToRow(); + const [rowIds, setRowIds] = useState([] as string[]); + + // const navigateToRow = useNavigateToRow(); useEffect(() => { - if (!viewId || !rowIds.length) return; + if (!viewId) return; - void getViewRowsMap?.(viewId, rowIds).then(({ rows }) => { - setRows(rows); - }); - }, [getViewRowsMap, rowIds, viewId]); + const update = (meta: ViewMeta) => { + setRelations(meta.database_relations); + }; + + try { + void loadViewMeta?.(viewId, update); + } catch (e) { + console.error(e); + } + }, [loadViewMeta, viewId]); + + const handleUpdateRowIds = useCallback( + (rows: DatabaseContextState['rowDocMap']) => { + const ids = (cell.data?.toJSON() as RelationCellData) ?? []; + + setRowIds(ids.filter((id) => rows?.has(id))); + }, + [cell.data] + ); + + useEffect(() => { + if (!relatedViewId || !getViewRowsMap || !relatedFieldId) return; + void (async () => { + try { + const { rows } = await getViewRowsMap(relatedViewId); + + setRows(rows); + handleUpdateRowIds(rows); + } catch (e) { + console.error(e); + } + })(); + }, [getViewRowsMap, relatedViewId, relatedFieldId, handleUpdateRowIds]); + + useEffect(() => { + const observerHandler = () => (rows ? handleUpdateRowIds(rows) : setRowIds([])); + + rows?.observe(observerHandler); + return () => rows?.unobserve(observerHandler); + }, [rows, handleUpdateRowIds]); + + useEffect(() => { + if (!relatedViewId) return; + + void (async () => { + try { + const viewDoc = await loadView?.(relatedViewId); + + if (!viewDoc) { + throw new Error('No access'); + } + + const database = viewDoc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase; + const fieldId = getPrimaryFieldId(database); + + setNoAccess(!fieldId); + setRelatedFieldId(fieldId); + } catch (e) { + console.error(e); + setNoAccess(true); + } + })(); + }, [loadView, relatedViewId]); return (
- {rowIds.map((rowId) => { - const rowDoc = rows?.get(rowId); + {noAccess ? ( +
No access
+ ) : ( + rowIds.map((rowId) => { + const rowDoc = rows?.get(rowId) as YDoc; - return ( -
{ - e.stopPropagation(); - navigateToRow?.(rowId); - }} - className={'w-full cursor-pointer underline'} - > - {rowDoc && } -
- ); - })} + return ( +
{ + // e.stopPropagation(); + // navigateToRow?.(rowId); + // }} + className={'underline'} + > + +
+ ); + }) + )}
); } diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx index cb10266e23..d01e54f76a 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx @@ -15,9 +15,9 @@ export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldI }; onRowChange(); - data?.observe(onRowChange); + data?.observeDeep(onRowChange); return () => { - data?.unobserve(onRowChange); + data?.unobserveDeep(onRowChange); }; }, [rowDoc]); diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx index 9538582f11..20ef342be9 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx @@ -31,10 +31,7 @@ export function SelectOptionCell({ cell, fieldId, style, placeholder }: CellProp ) : null; return ( -
+
{renderSelectedOptions(selectOptionIds)}
); diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx index 6a30c4c9d2..58c20a31e7 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx @@ -1,13 +1,20 @@ import { useReadOnly } from '@/application/database-yjs'; import { CellProps, UrlCell as UrlCellType } from '@/application/database-yjs/cell.type'; +import { notify } from '@/components/_shared/notify'; +import { copyTextToClipboard } from '@/utils/copy'; import { openUrl, processUrl } from '@/utils/url'; +import { IconButton, Tooltip } from '@mui/material'; import React, { useMemo } from 'react'; +import { ReactComponent as LinkSvg } from '@/assets/link.svg'; +import { ReactComponent as CopySvg } from '@/assets/copy.svg'; +import { useTranslation } from 'react-i18next'; export function UrlCell({ cell, style, placeholder }: CellProps) { const readOnly = useReadOnly(); const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]); + const [showActions, setShowActions] = React.useState(false); const className = useMemo(() => { const classList = ['select-text', 'w-fit', 'flex', 'w-full', 'items-center']; @@ -20,6 +27,8 @@ export function UrlCell({ cell, style, placeholder }: CellProps) { return classList.join(' '); }, [isUrl]); + const { t } = useTranslation(); + if (!cell?.data) return placeholder ? (
@@ -30,6 +39,8 @@ export function UrlCell({ cell, style, placeholder }: CellProps) { return (
setShowActions(true)} + onMouseLeave={() => setShowActions(false)} onClick={(e) => { if (!isUrl || !cell) return; if (readOnly) { @@ -40,6 +51,37 @@ export function UrlCell({ cell, style, placeholder }: CellProps) { className={className} > {cell?.data} + {showActions && isUrl && ( +
+ + { + e.stopPropagation(); + void openUrl(cell.data, '_blank'); + }} + > + + + + + { + e.stopPropagation(); + await copyTextToClipboard(cell.data); + notify.success(t('grid.url.copy')); + }} + > + + + +
+ )}
); } diff --git a/frontend/appflowy_web_app/src/components/database/components/field/CardField.tsx b/frontend/appflowy_web_app/src/components/database/components/field/CardField.tsx index 2575aa47ca..6f98f7579d 100644 --- a/frontend/appflowy_web_app/src/components/database/components/field/CardField.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/field/CardField.tsx @@ -1,10 +1,10 @@ import { YjsDatabaseKey } from '@/application/collab.type'; -import { useCellSelector, useFieldSelector } from '@/application/database-yjs'; +import { FieldType, useCellSelector, useFieldSelector } from '@/application/database-yjs'; import Cell from '@/components/database/components/cell/Cell'; -import React, { useMemo } from 'react'; +import React, { CSSProperties, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -function CardField({ rowId, fieldId, index }: { rowId: string; fieldId: string; index: number }) { +function CardField({ rowId, fieldId }: { rowId: string; fieldId: string; index: number }) { const { t } = useTranslation(); const { field } = useFieldSelector(fieldId); const cell = useCellSelector({ @@ -13,8 +13,26 @@ function CardField({ rowId, fieldId, index }: { rowId: string; fieldId: string; }); const isPrimary = field?.get(YjsDatabaseKey.is_primary); + const type = field?.get(YjsDatabaseKey.type); const style = useMemo(() => { - const styleProperties = {}; + const styleProperties: CSSProperties = { + overflow: 'hidden', + width: '100%', + textAlign: 'left', + }; + + if ([FieldType.Relation, FieldType.SingleSelect, FieldType.MultiSelect].includes(Number(type))) { + Object.assign(styleProperties, { + breakWord: 'break-word', + whiteSpace: 'normal', + flexWrap: 'wrap', + }); + } else { + Object.assign(styleProperties, { + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }); + } if (isPrimary) { Object.assign(styleProperties, { @@ -23,14 +41,8 @@ function CardField({ rowId, fieldId, index }: { rowId: string; fieldId: string; }); } - if (index !== 0) { - Object.assign(styleProperties, { - marginTop: '8px', - }); - } - return styleProperties; - }, [index, isPrimary]); + }, [isPrimary, type]); if (isPrimary && !cell?.data) { return ( @@ -40,6 +52,17 @@ function CardField({ rowId, fieldId, index }: { rowId: string; fieldId: string; ); } + if (Number(type) === FieldType.Checkbox) { + return ( +
+ + + + {field?.get(YjsDatabaseKey.name) || ''} +
+ ); + } + return ; } diff --git a/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx b/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx index 3749e21afd..78f380b73e 100644 --- a/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx @@ -1,16 +1,18 @@ import { FieldType } from '@/application/database-yjs/database.type'; import { FC, memo } from 'react'; -import { ReactComponent as TextSvg } from '$icons/16x/text.svg'; -import { ReactComponent as NumberSvg } from '$icons/16x/number.svg'; -import { ReactComponent as DateSvg } from '$icons/16x/date.svg'; -import { ReactComponent as SingleSelectSvg } from '$icons/16x/single_select.svg'; -import { ReactComponent as MultiSelectSvg } from '$icons/16x/multiselect.svg'; -import { ReactComponent as ChecklistSvg } from '$icons/16x/checklist.svg'; -import { ReactComponent as CheckboxSvg } from '$icons/16x/checkbox.svg'; -import { ReactComponent as URLSvg } from '$icons/16x/url.svg'; -import { ReactComponent as LastEditedTimeSvg } from '$icons/16x/last_modified.svg'; -import { ReactComponent as CreatedSvg } from '$icons/16x/created_at.svg'; -import { ReactComponent as RelationSvg } from '$icons/16x/relation.svg'; +import { ReactComponent as TextSvg } from '@/assets/text.svg'; +import { ReactComponent as NumberSvg } from '@/assets/number.svg'; +import { ReactComponent as DateSvg } from '@/assets/date.svg'; +import { ReactComponent as SingleSelectSvg } from '@/assets/single_select.svg'; +import { ReactComponent as MultiSelectSvg } from '@/assets/multiselect.svg'; +import { ReactComponent as ChecklistSvg } from '@/assets/checklist.svg'; +import { ReactComponent as CheckboxSvg } from '@/assets/checkbox.svg'; +import { ReactComponent as URLSvg } from '@/assets/url.svg'; +import { ReactComponent as LastEditedTimeSvg } from '@/assets/last_modified.svg'; +import { ReactComponent as CreatedSvg } from '@/assets/created_at.svg'; +import { ReactComponent as RelationSvg } from '@/assets/relation.svg'; +import { ReactComponent as AISummariesSvg } from '@/assets/ai_summary.svg'; +import { ReactComponent as AITranslationsSvg } from '@/assets/ai_translate.svg'; export const FieldTypeSvgMap: Record>> = { [FieldType.RichText]: TextSvg, @@ -24,10 +26,13 @@ export const FieldTypeSvgMap: Record [FieldType.LastEditedTime]: LastEditedTimeSvg, [FieldType.CreatedTime]: CreatedSvg, [FieldType.Relation]: RelationSvg, + [FieldType.AISummaries]: AISummariesSvg, + [FieldType.AITranslations]: AITranslationsSvg, }; export const FieldTypeIcon: FC<{ type: FieldType; className?: string }> = memo(({ type, ...props }) => { const Svg = FieldTypeSvgMap[type]; + if (!Svg) return null; return ; }); diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx index 56845e8425..7f022aa34a 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx @@ -4,6 +4,7 @@ import { Column, useFieldSelector } from '@/application/database-yjs/selector'; import { FieldTypeIcon } from '@/components/database/components/field'; import { Tooltip } from '@mui/material'; import React, { useMemo } from 'react'; +import { ReactComponent as AIIndicatorSvg } from '@/assets/ai_indicator.svg'; export function GridColumn({ column, index }: { column: Column; index: number }) { const { field } = useFieldSelector(column.fieldId); @@ -16,20 +17,23 @@ export function GridColumn({ column, index }: { column: Column; index: number }) return parseInt(type) as FieldType; }, [field]); + const isAIField = [FieldType.AISummaries, FieldType.AITranslations].includes(type); + return (
{name}
+ {isAIField && }
); diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx index 7bacc7b882..f932cd3a72 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx @@ -27,19 +27,19 @@ export function useRenderFields() { })); return [ - { - type: GridColumnType.Action, - width: 64, - }, + // { + // type: GridColumnType.Action, + // width: 64, + // }, ...data, { type: GridColumnType.NewProperty, width: 150, }, - { - type: GridColumnType.Action, - width: 64, - }, + // { + // type: GridColumnType.Action, + // width: 64, + // }, ].filter(Boolean) as RenderColumn[]; }, [fields]); diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx index 30dd0ffe9e..2f269f823a 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx @@ -1,4 +1,4 @@ -import React, { memo, useEffect, useRef } from 'react'; +import React, { memo, useCallback, useEffect, useRef } from 'react'; import { areEqual, GridChildComponentProps, VariableSizeGrid } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GridColumnType, RenderColumn, GridColumn } from '../grid-column'; @@ -36,15 +36,17 @@ export const GridHeader = ({ scrollLeft, onScrollLeft, columnWidth, columns }: G } }, [scrollLeft]); + const resetGrid = useCallback(() => { + ref.current?.resetAfterIndices({ columnIndex: 0, rowIndex: 0 }); + }, []); + useEffect(() => { - if (ref.current) { - ref.current?.resetAfterIndices({ columnIndex: 0, rowIndex: 0 }); - } - }, [columns]); + resetGrid(); + }, [columns, resetGrid]); return (
- + {({ height, width }: { height: number; width: number }) => { return ( 36} rowCount={1} columnCount={columns.length} - columnWidth={(index) => columnWidth(index, width)} + columnWidth={(index) => { + return columnWidth(index, width); + }} ref={ref} onScroll={(props) => { onScrollLeft(props.scrollLeft); diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx index 4d7abb7a2c..ff2b7aaa40 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx @@ -2,38 +2,44 @@ import { YjsDatabaseKey } from '@/application/collab.type'; import { useDatabaseView } from '@/application/database-yjs'; import { CalculationType } from '@/application/database-yjs/database.type'; import { CalculationCell, ICalculationCell } from '../grid-calculation-cell'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; export interface GridCalculateRowCellProps { fieldId: string; } export function GridCalculateRowCell({ fieldId }: GridCalculateRowCellProps) { - const calculations = useDatabaseView()?.get(YjsDatabaseKey.calculations); + const databaseView = useDatabaseView(); const [calculation, setCalculation] = useState(); - useEffect(() => { + const handleObserver = useCallback(() => { + const calculations = databaseView?.get(YjsDatabaseKey.calculations); + if (!calculations) return; + calculations.forEach((calculation) => { + if (calculation.get(YjsDatabaseKey.field_id) === fieldId) { + setCalculation({ + id: calculation.get(YjsDatabaseKey.id), + fieldId: calculation.get(YjsDatabaseKey.field_id), + value: calculation.get(YjsDatabaseKey.calculation_value), + type: Number(calculation.get(YjsDatabaseKey.type)) as CalculationType, + }); + } + }); + }, [databaseView, fieldId]); + + useEffect(() => { const observerHandle = () => { - calculations.forEach((calculation) => { - if (calculation.get(YjsDatabaseKey.field_id) === fieldId) { - setCalculation({ - id: calculation.get(YjsDatabaseKey.id), - fieldId: calculation.get(YjsDatabaseKey.field_id), - value: calculation.get(YjsDatabaseKey.calculation_value), - type: Number(calculation.get(YjsDatabaseKey.type)) as CalculationType, - }); - } - }); + handleObserver(); }; observerHandle(); - calculations.observeDeep(observerHandle); + databaseView?.observeDeep(handleObserver); return () => { - calculations.unobserveDeep(observerHandle); + databaseView?.observeDeep(handleObserver); }; - }, [calculations, fieldId]); + }, [databaseView, fieldId, handleObserver]); return ; } diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx index ce09cb500f..d439681b21 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx @@ -31,11 +31,13 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr } }, [scrollLeft]); + const resetGrid = useCallback(() => { + ref.current?.resetAfterIndices({ columnIndex: 0, rowIndex: 0 }); + }, []); + useEffect(() => { - if (ref.current) { - ref.current.resetAfterIndices({ columnIndex: 0, rowIndex: 0 }); - } - }, [columns]); + resetGrid(); + }, [columns, resetGrid]); const getItemKey = useCallback( ({ columnIndex, rowIndex }: { columnIndex: number; rowIndex: number }) => { @@ -85,7 +87,7 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr
+ {({ height, width }: { height: number; width: number }) => ( columnWidth(index, width)} rowHeight={rowHeight} - className={'grid-table'} + className={'grid-table pb-6'} overscanRowCount={5} overscanColumnCount={5} style={{ diff --git a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx index 5f99969abf..24b3b5bf20 100644 --- a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx @@ -18,7 +18,7 @@ function DatabaseHeader({ return (
diff --git a/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx b/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx index c8e5f34a43..51eac87e58 100644 --- a/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx @@ -44,6 +44,8 @@ export function Property({ fieldId, rowId }: { fieldId: string; rowId: string }) case FieldType.Relation: return RelationCell; case FieldType.RichText: + case FieldType.AISummaries: + case FieldType.AITranslations: return TextCell; default: return TextProperty; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx b/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx index c930365b37..4932d0e034 100644 --- a/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx @@ -4,10 +4,10 @@ import React from 'react'; function PropertyWrapper({ fieldId, children }: { fieldId: string; children: React.ReactNode }) { return (
-
+
-
{children}
+
{children}
); } diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx index e5dd601c64..c74165fa66 100644 --- a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx @@ -6,9 +6,9 @@ import { forwardRef, FunctionComponent, SVGProps, useMemo } from 'react'; import { ViewTabs, ViewTab } from './ViewTabs'; import { useTranslation } from 'react-i18next'; -import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; -import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; -import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; +import { ReactComponent as GridSvg } from '@/assets/grid.svg'; +import { ReactComponent as BoardSvg } from '@/assets/board.svg'; +import { ReactComponent as CalendarSvg } from '@/assets/calendar.svg'; export interface DatabaseTabBarProps { viewIds: string[]; @@ -16,6 +16,7 @@ export interface DatabaseTabBarProps { setSelectedViewId?: (viewId: string) => void; viewName?: string; iidIndex: string; + hideConditions?: boolean; } const DatabaseIcons: { @@ -27,7 +28,7 @@ const DatabaseIcons: { }; export const DatabaseTabs = forwardRef( - ({ viewIds, viewName, iidIndex, selectedViewId, setSelectedViewId }, ref) => { + ({ viewIds, viewName, hideConditions, iidIndex, selectedViewId, setSelectedViewId }, ref) => { const { t } = useTranslation(); const view = useDatabaseView(); const views = useDatabase().get(YjsDatabaseKey.views); @@ -38,9 +39,7 @@ export const DatabaseTabs = forwardRef( }; const className = useMemo(() => { - const classList = [ - 'mx-16 -mb-[0.5px] flex items-center overflow-hidden border-line-divider text-text-title max-md:mx-4', - ]; + const classList = ['-mb-[0.5px] flex items-center overflow-hidden border-line-divider text-text-title']; if (layout === DatabaseViewLayout.Calendar) { classList.push('border-b'); @@ -91,7 +90,7 @@ export const DatabaseTabs = forwardRef( })}
- {layout !== DatabaseViewLayout.Calendar ? : null} + {!hideConditions && layout !== DatabaseViewLayout.Calendar ? : null}
); } diff --git a/frontend/appflowy_web_app/src/components/document/Document.tsx b/frontend/appflowy_web_app/src/components/document/Document.tsx index 15188e2ba2..2285237c21 100644 --- a/frontend/appflowy_web_app/src/components/document/Document.tsx +++ b/frontend/appflowy_web_app/src/components/document/Document.tsx @@ -1,27 +1,19 @@ -import { YDoc } from '@/application/collab.type'; -import { ViewMeta } from '@/application/db/tables/view_metas'; +import { GetViewRowsMap, LoadView, LoadViewMeta, YDoc } from '@/application/collab.type'; import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; import { Editor } from '@/components/editor'; import React, { Suspense } from 'react'; import ViewMetaPreview, { ViewMetaProps } from '@/components/view-meta/ViewMetaPreview'; -import Y from 'yjs'; -export interface DocumentProps extends ViewMetaProps { +export interface DocumentProps { doc: YDoc; navigateToView?: (viewId: string) => Promise; - loadViewMeta?: (viewId: string) => Promise; - loadView?: (viewId: string) => Promise; - getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map; destroy: () => void }>; + loadViewMeta?: LoadViewMeta; + loadView?: LoadView; + getViewRowsMap?: GetViewRowsMap; + viewMeta: ViewMetaProps; } -export const Document = ({ - doc, - loadView, - navigateToView, - loadViewMeta, - getViewRowsMap, - ...viewMeta -}: DocumentProps) => { +export const Document = ({ doc, loadView, navigateToView, loadViewMeta, getViewRowsMap, viewMeta }: DocumentProps) => { return (
diff --git a/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx b/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx index 5b8eb3431d..08fa513951 100644 --- a/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx +++ b/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx @@ -1,7 +1,5 @@ -import { FontLayout, LineHeightLayout, YDoc } from '@/application/collab.type'; -import { ViewMeta } from '@/application/db/tables/view_metas'; +import { FontLayout, GetViewRowsMap, LineHeightLayout, LoadView, LoadViewMeta } from '@/application/collab.type'; import { createContext, useContext } from 'react'; -import Y from 'yjs'; export interface EditorLayoutStyle { fontLayout: FontLayout; @@ -21,9 +19,9 @@ export interface EditorContextState { codeGrammars?: Record; addCodeGrammars?: (blockId: string, grammar: string) => void; navigateToView?: (viewId: string) => Promise; - loadViewMeta?: (viewId: string) => Promise; - loadView?: (viewId: string) => Promise; - getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map; destroy: () => void }>; + loadViewMeta?: LoadViewMeta; + loadView?: LoadView; + getViewRowsMap?: GetViewRowsMap; } export const EditorContext = createContext({ diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx index e5c36eed2d..2c5091a910 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx @@ -1,4 +1,5 @@ import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg'; +import { ViewMeta } from '@/application/db/tables/view_metas'; import { useEditorContext } from '@/components/editor/EditorContext'; import { Database } from '@/components/database'; import { DatabaseNode, EditorElementProps } from '@/components/editor/editor.type'; @@ -27,7 +28,7 @@ export const DatabaseBlock = memo( switch (type) { case BlockType.GridBlock: Object.assign(style, { - height: 360, + height: 400, }); break; case BlockType.CalendarBlock: @@ -57,6 +58,37 @@ export const DatabaseBlock = memo( })(); }, [viewId, loadView]); + const [selectedViewId, setSelectedViewId] = useState(); + const [visibleViewIds, setVisibleViewIds] = useState([]); + const [iidName, setIidName] = useState(''); + + useEffect(() => { + const updateVisibleViewIds = async (meta: ViewMeta) => { + const viewIds = meta.visible_view_ids || []; + + if (!viewIds.includes(viewId)) { + setSelectedViewId(meta.visible_view_ids[0]); + } else { + setSelectedViewId(viewId); + } + + setIidName(meta.name); + setVisibleViewIds(viewIds); + }; + + void (async () => { + try { + const meta = await loadViewMeta?.(viewId, updateVisibleViewIds); + + if (meta) { + await updateVisibleViewIds(meta); + } + } catch (e) { + setNotFound(true); + } + })(); + }, [loadViewMeta, viewId]); + return ( <>
{children}
-
- {viewId && doc ? ( +
+ {selectedViewId && doc ? ( <> {isHovering && (
@@ -102,7 +139,7 @@ export const DatabaseBlock = memo( > {notFound ? ( <> -
{t('publish.databaseHasNotBeenPublished')}
+
{t('publish.hasNotBeenPublished')}
) : ( diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx index 1ea166b3b1..fd167985af 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx @@ -1,6 +1,6 @@ import { renderDate } from '@/utils/time'; import React, { useMemo } from 'react'; -import { ReactComponent as DateSvg } from '$icons/16x/date.svg'; +import { ReactComponent as DateSvg } from '@/assets/date.svg'; import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg'; function MentionDate({ date, reminder }: { date: string; reminder?: { id: string; option: string } }) { diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx index 0d5a53a983..cdf4f46f62 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx @@ -7,15 +7,16 @@ import { useTranslation } from 'react-i18next'; function MentionPage({ pageId }: { pageId: string }) { const context = useEditorContext(); - const { navigateToView, loadViewMeta } = context; + const { navigateToView, loadViewMeta, loadView } = context; const [unPublished, setUnPublished] = useState(false); const [meta, setMeta] = useState(null); useEffect(() => { void (async () => { - if (loadViewMeta) { + if (loadViewMeta && loadView) { setUnPublished(false); try { + await loadView(pageId); const meta = await loadViewMeta(pageId); setMeta(meta); @@ -24,7 +25,7 @@ function MentionPage({ pageId }: { pageId: string }) { } } })(); - }, [loadViewMeta, pageId]); + }, [loadViewMeta, pageId, loadView]); const icon = useMemo(() => { return meta?.icon; diff --git a/frontend/appflowy_web_app/src/components/login/Login.tsx b/frontend/appflowy_web_app/src/components/login/Login.tsx index 76b8e9faa5..bd8c166ff6 100644 --- a/frontend/appflowy_web_app/src/components/login/Login.tsx +++ b/frontend/appflowy_web_app/src/components/login/Login.tsx @@ -10,11 +10,11 @@ import { useSearchParams } from 'react-router-dom'; export function Login() { const { t } = useTranslation(); const [search] = useSearchParams(); - const redirectTo = search.get('redirectTo') || window.location.href; + const redirectTo = search.get('redirectTo') || ''; const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false; useEffect(() => { - if (isAuthenticated && encodeURIComponent(redirectTo) !== window.location.href) { + if (isAuthenticated && redirectTo && encodeURIComponent(redirectTo) !== window.location.href) { window.location.href = redirectTo; } }, [isAuthenticated, redirectTo]); diff --git a/frontend/appflowy_web_app/src/components/publish/CollabView.tsx b/frontend/appflowy_web_app/src/components/publish/CollabView.tsx index 0c31919813..00a49afe82 100644 --- a/frontend/appflowy_web_app/src/components/publish/CollabView.tsx +++ b/frontend/appflowy_web_app/src/components/publish/CollabView.tsx @@ -1,22 +1,19 @@ -import { ViewLayout, YDoc } from '@/application/collab.type'; -import { ViewMeta } from '@/application/db/tables/view_metas'; +import { GetViewRowsMap, LoadView, LoadViewMeta, ViewLayout, YDoc } from '@/application/collab.type'; import { usePublishContext } from '@/application/publish'; import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; -import { useAppThemeMode } from '@/components/app/useAppThemeMode'; -import { Database } from '@/components/database'; import { Document } from '@/components/document'; +import DatabaseView from '@/components/publish/DatabaseView'; import { useViewMeta } from '@/components/publish/useViewMeta'; import React, { useMemo } from 'react'; -import { ViewMetaProps } from 'src/components/view-meta'; -import Y from 'yjs'; +import { ViewMetaProps } from '@/components/view-meta'; export interface CollabViewProps { doc?: YDoc; } function CollabView({ doc }: CollabViewProps) { + const visibleViewIds = usePublishContext()?.viewMeta?.visible_view_ids; const { viewId, layout, icon, cover, layoutClassName, style, name } = useViewMeta(); - const { isDark } = useAppThemeMode(); const View = useMemo(() => { switch (layout) { case ViewLayout.Document: @@ -24,20 +21,18 @@ function CollabView({ doc }: CollabViewProps) { case ViewLayout.Grid: case ViewLayout.Board: case ViewLayout.Calendar: - return Database; + return DatabaseView; default: return null; } - }, [layout]) as React.FC< - { - doc: YDoc; - isDark: boolean; - navigateToView?: (viewId: string) => Promise; - loadViewMeta?: (viewId: string) => Promise; - getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map; destroy: () => void }>; - loadView?: (id: string) => Promise; - } & ViewMetaProps - >; + }, [layout]) as React.FC<{ + doc: YDoc; + navigateToView?: (viewId: string) => Promise; + loadViewMeta?: LoadViewMeta; + getViewRowsMap?: GetViewRowsMap; + loadView?: LoadView; + viewMeta: ViewMetaProps; + }>; const navigateToView = usePublishContext()?.toView; const loadViewMeta = usePublishContext()?.loadViewMeta; @@ -56,12 +51,14 @@ function CollabView({ doc }: CollabViewProps) { getViewRowsMap={getViewRowsMap} navigateToView={navigateToView} loadView={loadView} - icon={icon} - cover={cover} - viewId={viewId} - name={name} - isDark={isDark} - layout={layout || ViewLayout.Document} + viewMeta={{ + icon, + cover, + viewId, + name, + layout: layout || ViewLayout.Document, + visibleViewIds: visibleViewIds || [], + }} />
); diff --git a/frontend/appflowy_web_app/src/components/publish/DatabaseView.tsx b/frontend/appflowy_web_app/src/components/publish/DatabaseView.tsx new file mode 100644 index 0000000000..3ffb980c34 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/DatabaseView.tsx @@ -0,0 +1,69 @@ +import { GetViewRowsMap, LoadView, LoadViewMeta, YDoc } from '@/application/collab.type'; +import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; +import { Database } from '@/components/database'; +import DatabaseHeader from '@/components/database/components/header/DatabaseHeader'; +import { ViewMetaProps } from '@/components/view-meta'; +import React, { Suspense, useCallback, useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +export interface DatabaseProps { + doc: YDoc; + getViewRowsMap?: GetViewRowsMap; + loadView?: LoadView; + navigateToView?: (viewId: string) => Promise; + loadViewMeta?: LoadViewMeta; + viewMeta: ViewMetaProps; +} + +function DatabaseView({ viewMeta, ...props }: DatabaseProps) { + const [search, setSearch] = useSearchParams(); + const visibleViewIds = useMemo(() => viewMeta.visibleViewIds || [], [viewMeta]); + + const iidIndex = viewMeta.viewId; + const viewId = useMemo(() => { + return search.get('v') || iidIndex; + }, [search, iidIndex]); + + const handleChangeView = useCallback( + (viewId: string) => { + setSearch({ v: viewId }); + }, + [setSearch] + ); + + const handleNavigateToRow = useCallback( + (rowId: string) => { + setSearch({ r: rowId }); + }, + [setSearch] + ); + + const rowId = search.get('r') || undefined; + + if (!viewId) return null; + + return ( +
+ + }> + + +
+ ); +} + +export default DatabaseView; diff --git a/frontend/appflowy_web_app/src/components/view-meta/ViewCover.tsx b/frontend/appflowy_web_app/src/components/view-meta/ViewCover.tsx index df7c495bb3..a22a3f6ab7 100644 --- a/frontend/appflowy_web_app/src/components/view-meta/ViewCover.tsx +++ b/frontend/appflowy_web_app/src/components/view-meta/ViewCover.tsx @@ -22,7 +22,12 @@ function ViewCover({ coverValue, coverType }: { coverValue?: string; coverType?: } return ( -
+
{coverType === 'color' && renderCoverColor(coverValue)} {(coverType === 'custom' || coverType === 'built_in') && renderCoverImage(coverValue)}
diff --git a/frontend/appflowy_web_app/src/components/view-meta/ViewMetaPreview.tsx b/frontend/appflowy_web_app/src/components/view-meta/ViewMetaPreview.tsx index 67310f4036..e40770dc33 100644 --- a/frontend/appflowy_web_app/src/components/view-meta/ViewMetaPreview.tsx +++ b/frontend/appflowy_web_app/src/components/view-meta/ViewMetaPreview.tsx @@ -20,6 +20,7 @@ export interface ViewMetaProps { name?: string; viewId?: string; layout?: ViewLayout; + visibleViewIds?: string[]; } export function ViewMetaPreview({ icon, cover, name }: ViewMetaProps) { diff --git a/frontend/appflowy_web_app/src/pages/AfterPaymentPage.tsx b/frontend/appflowy_web_app/src/pages/AfterPaymentPage.tsx new file mode 100644 index 0000000000..7395045088 --- /dev/null +++ b/frontend/appflowy_web_app/src/pages/AfterPaymentPage.tsx @@ -0,0 +1,41 @@ +import { openOrDownload } from '@/components/publish/header/utils'; +import { Button, Typography } from '@mui/material'; +import React from 'react'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; +import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg'; + +function AfterPaymentPage() { + return ( +
+
+ + <> + + + + +
+ Explore features in your new plan +
+
+
+ Congratulations! You just unlocked more workspace members and unlimited AI + responses. 🎉 +
+
+ +
+
+ ); +} + +export default AfterPaymentPage; diff --git a/frontend/appflowy_web_app/src/styles/mixin.scss b/frontend/appflowy_web_app/src/styles/mixin.scss index d571ef9cbf..50ef277fe2 100644 --- a/frontend/appflowy_web_app/src/styles/mixin.scss +++ b/frontend/appflowy_web_app/src/styles/mixin.scss @@ -9,8 +9,8 @@ @mixin scrollbar-style { ::-webkit-scrollbar, &::-webkit-scrollbar { - width: 4px; - height: 4px; + width: 8px; + height: 8px; } &:hover { diff --git a/frontend/appflowy_web_app/src/utils/url.ts b/frontend/appflowy_web_app/src/utils/url.ts index a10cf9ca85..7c57b8888a 100644 --- a/frontend/appflowy_web_app/src/utils/url.ts +++ b/frontend/appflowy_web_app/src/utils/url.ts @@ -20,6 +20,14 @@ export function processUrl(input: string) { return processedUrl; } + if (input.startsWith('http')) { + return processedUrl; + } + + if (input.startsWith('localhost')) { + return `http://${input}`; + } + const domain = input.split('/')[0]; if (isIP(domain) || isFQDN(domain)) { diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 9d58581071..2571e7ead1 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -327,6 +327,7 @@ "helpCenter": "Help Center", "add": "Add", "yes": "Yes", + "no": "No", "clear": "Clear", "remove": "Remove", "dontRemove": "Don't remove",