feat: support document apply remote events (#5436)
* feat: support document apply remote events * fix: add tests for database * fix: add test for filter,sort and group * fix: jest ci * fix: jest ci * fix: jest ci * fix: jest ci * fix: cypress test
4
.github/workflows/web2_ci.yaml
vendored
@ -52,11 +52,11 @@ jobs:
|
|||||||
working-directory: frontend/appflowy_web_app
|
working-directory: frontend/appflowy_web_app
|
||||||
run: |
|
run: |
|
||||||
pnpm install
|
pnpm install
|
||||||
- name: test and lint
|
- name: Run lint check
|
||||||
working-directory: frontend/appflowy_web_app
|
working-directory: frontend/appflowy_web_app
|
||||||
run: |
|
run: |
|
||||||
pnpm run lint
|
pnpm run lint
|
||||||
pnpm run test:unit
|
|
||||||
- name: build and analyze
|
- name: build and analyze
|
||||||
working-directory: frontend/appflowy_web_app
|
working-directory: frontend/appflowy_web_app
|
||||||
run: |
|
run: |
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
name: Cypress Tests
|
name: Web Code Coverage
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
@ -13,7 +13,7 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
cypress-run:
|
test:
|
||||||
if: github.event.pull_request.draft != true
|
if: github.event.pull_request.draft != true
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
@ -46,3 +46,14 @@ jobs:
|
|||||||
build: pnpm run build
|
build: pnpm run build
|
||||||
start: pnpm run start
|
start: pnpm run start
|
||||||
browser: chrome
|
browser: chrome
|
||||||
|
|
||||||
|
- name: Jest run
|
||||||
|
working-directory: frontend/appflowy_web_app
|
||||||
|
run: |
|
||||||
|
pnpm run test:unit
|
||||||
|
|
||||||
|
- name: Generate and post coverage summary
|
||||||
|
working-directory: frontend/appflowy_web_app
|
||||||
|
run: |
|
||||||
|
pnpm run merge-coverage
|
||||||
|
|
@ -7,3 +7,4 @@ tsconfig.json
|
|||||||
vite.config.ts
|
vite.config.ts
|
||||||
**/*.cy.tsx
|
**/*.cy.tsx
|
||||||
*.config.ts
|
*.config.ts
|
||||||
|
coverage/
|
@ -5,3 +5,4 @@ src-tauri/
|
|||||||
tsconfig.json
|
tsconfig.json
|
||||||
src/application/services/tauri-services/
|
src/application/services/tauri-services/
|
||||||
vite.config.ts
|
vite.config.ts
|
||||||
|
coverage/
|
3
frontend/appflowy_web_app/.gitignore
vendored
@ -30,3 +30,6 @@ src/application/services/tauri-services/backend/models/
|
|||||||
src/application/services/tauri-services/backend/events/
|
src/application/services/tauri-services/backend/events/
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
22
frontend/appflowy_web_app/.nycrc
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"all": true,
|
||||||
|
"extends": "@istanbuljs/nyc-config-typescript",
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.tsx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"cypress/**/*.*",
|
||||||
|
"**/*.d.ts",
|
||||||
|
"**/*.cy.tsx",
|
||||||
|
"**/*.cy.ts"
|
||||||
|
],
|
||||||
|
"reporter": [
|
||||||
|
"text",
|
||||||
|
"html",
|
||||||
|
"text-summary",
|
||||||
|
"json"
|
||||||
|
],
|
||||||
|
"temp-dir": "coverage/.nyc_output",
|
||||||
|
"report-dir": "coverage/cypress"
|
||||||
|
}
|
@ -1,11 +1,22 @@
|
|||||||
import { defineConfig } from 'cypress';
|
import { defineConfig } from 'cypress';
|
||||||
|
import registerCodeCoverageTasks from '@cypress/code-coverage/task';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
env: {
|
||||||
|
codeCoverage: {
|
||||||
|
exclude: ['cypress/**/*.*', '**/__tests__/**/*.*', '**/*.test.*'],
|
||||||
|
},
|
||||||
|
},
|
||||||
component: {
|
component: {
|
||||||
devServer: {
|
devServer: {
|
||||||
framework: 'react',
|
framework: 'react',
|
||||||
bundler: 'vite',
|
bundler: 'vite',
|
||||||
},
|
},
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
registerCodeCoverageTasks(on, config);
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
supportFile: 'cypress/support/component.ts',
|
||||||
},
|
},
|
||||||
retries: {
|
retries: {
|
||||||
// Configure retry attempts for `cypress run`
|
// Configure retry attempts for `cypress run`
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "9eebea03-3ed5-4298-86b2-a7f77856d48b",
|
||||||
|
"name": "workspace",
|
||||||
|
"icon": "",
|
||||||
|
"owner": {
|
||||||
|
"id": 0,
|
||||||
|
"name": "system"
|
||||||
|
},
|
||||||
|
"type": 0,
|
||||||
|
"workspaceDatabaseId": "375874be-7a4f-4b7c-8b89-1dc9a39838f4"
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
[{"database_id":"037a985f-f369-4c4a-8011-620012850a68","created_at":"1713429700","views":["48c52cf7-bf98-43fa-96ad-b31aade9b071"]},{"database_id":"daea6aee-9365-4703-a8e2-a2fa6a07b214","created_at":"1714449533","views":["b6347acb-3174-4f0e-98e9-dcce07e5dbf7"]},{"database_id":"4c658817-20db-4f56-b7f9-0637a22dfeb6","created_at":"0","views":["7d2148fc-cace-4452-9c5c-96e52e6bf8b5","e410747b-5f2f-45a0-b2f7-890ad3001355","2143e95d-5dcb-4e0f-bb2c-50944e6e019f","a5566e49-f156-4168-9b2d-17926c5da329","135615fa-66f7-4451-9b54-d7e99445fca4","b4e77203-5c8b-48df-bbc5-2e1143eb0e61","a6af311f-cbc8-42c2-b801-7115619c3776"]},{"database_id":"4c658817-20db-4f56-b7f9-0637a22dfeb6","created_at":"0","views":["7d2148fc-cace-4452-9c5c-96e52e6bf8b5","e97877f5-c365-4025-9e6a-e590c4b19dbb","f0c59921-04ee-4971-995c-79b7fd8c00e2","7eb697cd-6a55-40bb-96ac-0d4a3bc924b2"]},{"database_id":"ee63da2b-aa2a-4d0b-aab0-59008635363a","created_at":"0","views":["2c1ee95a-1b09-4a1f-8d5e-501bc4861a9d","91ea7c08-f6b3-4b81-aa1e-d3664686186f"]},{"database_id":"e788f014-d0d3-4dfe-81ef-aa1ebb4d6366","created_at":"0","views":["1b0e322d-4909-4c63-914a-d034fc363097","350f425b-b671-4e2d-8182-5998a6e62924"]},{"database_id":"ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d","created_at":"0","views":["0ce13415-6cce-4497-94c6-475ad96c249e","e4c89421-12b2-4d02-863d-20949eec9271"]},{"database_id":"ce267d12-3b61-4ebb-bb03-d65272f5f817","created_at":"0","views":["ee3ae8ce-959a-4df3-8734-40b535ff88e3","66a6f3bc-c78f-4f74-a09e-08d4717bf1fd","2bf50c03-f41f-4363-b5b1-101216a6c5cc"]}]
|
@ -0,0 +1 @@
|
|||||||
|
{"208d248f-5c08-4be5-a022-e0a97c2d705e":[16,1,162,212,253,234,14,0,161,166,231,212,218,8,3,39,1,245,198,128,205,14,0,161,233,140,128,164,8,5,2,1,165,222,139,132,12,0,161,128,181,233,166,8,1,7,1,179,227,145,238,11,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,10,2,213,228,161,169,9,0,161,233,140,128,164,8,5,1,161,245,198,128,205,14,1,9,2,185,222,141,169,9,0,161,140,225,231,182,6,2,4,168,185,222,141,169,9,3,1,122,0,0,0,0,102,88,52,85,1,138,182,251,229,8,0,161,162,212,253,234,14,38,7,1,166,231,212,218,8,0,161,165,222,139,132,12,6,4,1,128,181,233,166,8,0,161,179,227,145,238,11,9,2,1,233,140,128,164,8,0,161,221,230,177,144,4,1,6,1,239,245,240,149,8,0,161,157,238,145,201,3,1,2,1,140,225,231,182,6,0,161,239,245,240,149,8,1,3,1,246,148,237,174,6,0,161,138,182,251,229,8,6,5,16,221,174,135,220,5,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,221,174,135,220,5,0,2,105,100,1,119,36,50,48,56,100,50,52,56,102,45,53,99,48,56,45,52,98,101,53,45,97,48,50,50,45,101,48,97,57,55,99,50,100,55,48,53,101,40,0,221,174,135,220,5,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,97,100,55,100,99,52,53,98,45,52,52,98,53,45,52,57,56,102,45,98,102,97,50,45,48,102,52,51,98,102,48,53,99,99,48,100,40,0,221,174,135,220,5,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,221,174,135,220,5,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,221,174,135,220,5,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,39,162,40,0,221,174,135,220,5,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,76,39,162,39,0,221,174,135,220,5,0,5,99,101,108,108,115,1,39,0,221,174,135,220,5,9,6,121,52,52,50,48,119,1,40,0,221,174,135,220,5,10,4,100,97,116,97,1,119,4,117,76,117,51,40,0,221,174,135,220,5,10,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,39,0,221,174,135,220,5,9,6,51,111,45,90,115,109,1,40,0,221,174,135,220,5,13,4,100,97,116,97,1,119,6,67,97,114,100,32,49,40,0,221,174,135,220,5,13,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,0,1,221,230,177,144,4,0,161,246,148,237,174,6,4,2,1,157,238,145,201,3,0,161,213,228,161,169,9,9,2,15,128,181,233,166,8,1,0,2,162,212,253,234,14,1,0,39,165,222,139,132,12,1,0,7,166,231,212,218,8,1,0,4,233,140,128,164,8,1,0,6,138,182,251,229,8,1,0,7,140,225,231,182,6,1,0,3,239,245,240,149,8,1,0,2,179,227,145,238,11,1,0,10,245,198,128,205,14,1,0,2,246,148,237,174,6,1,0,5,213,228,161,169,9,1,0,10,185,222,141,169,9,1,0,4,221,230,177,144,4,1,0,2,157,238,145,201,3,1,0,2]}
|
1
frontend/appflowy_web_app/cypress/fixtures/folder.json
Normal file
@ -1,61 +1 @@
|
|||||||
{
|
{"data":{"user_profile":{"uid":304120109071339520,"uuid":"cbff060a-196d-415a-aa80-759c01886466","email":"lu@appflowy.io","password":"","name":"Kilu","metadata":{"icon_url":"🇽🇰"},"encryption_sign":null,"latest_workspace_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b","updated_at":1715847453},"visiting_workspace":{"workspace_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b","database_storage_id":"375874be-7a4f-4b7c-8b89-1dc9a39838f4","owner_uid":304120109071339520,"owner_name":"Kilu","workspace_type":0,"workspace_name":"Kilu Works","created_at":"2024-03-13T07:23:10.275174Z","icon":"😆"},"workspaces":[{"workspace_id":"81570fa8-8be9-4b2d-9f1c-1ef4f34079a8","database_storage_id":"6c1f1a2c-e8d5-4bc2-917f-495bce862abb","owner_uid":311828434584080384,"owner_name":"Zack Zi Xiang Fu","workspace_type":0,"workspace_name":"My Workspace","created_at":"2024-04-03T13:53:18.295918Z","icon":""},{"workspace_id":"fcb503f9-9287-4de4-8de0-ea191e680968","database_storage_id":"ae1b82a5-2b93-45c7-901a-f9357c544534","owner_uid":276169796100296704,"owner_name":"Annie Anqi Wang","workspace_type":0,"workspace_name":"AppFlowy Test","created_at":"2023-12-27T04:18:36.372013Z","icon":""},{"workspace_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b","database_storage_id":"375874be-7a4f-4b7c-8b89-1dc9a39838f4","owner_uid":304120109071339520,"owner_name":"Kilu","workspace_type":0,"workspace_name":"Kilu Works","created_at":"2024-03-13T07:23:10.275174Z","icon":"😆"}]},"code":0,"message":"Operation completed successfully."}
|
||||||
"data": {
|
|
||||||
"user_profile": {
|
|
||||||
"uid": 304120109071339520,
|
|
||||||
"uuid": "cbff060a-196d-415a-aa80-759c01886466",
|
|
||||||
"email": "lu@appflowy.io",
|
|
||||||
"password": "",
|
|
||||||
"name": "Kilu",
|
|
||||||
"metadata": {
|
|
||||||
"icon_url": "🇽🇰"
|
|
||||||
},
|
|
||||||
"encryption_sign": null,
|
|
||||||
"latest_workspace_id": "9eebea03-3ed5-4298-86b2-a7f77856d48b",
|
|
||||||
"updated_at": 1715847453
|
|
||||||
},
|
|
||||||
"visiting_workspace": {
|
|
||||||
"workspace_id": "9eebea03-3ed5-4298-86b2-a7f77856d48b",
|
|
||||||
"database_storage_id": "375874be-7a4f-4b7c-8b89-1dc9a39838f4",
|
|
||||||
"owner_uid": 304120109071339520,
|
|
||||||
"owner_name": "Kilu",
|
|
||||||
"workspace_type": 0,
|
|
||||||
"workspace_name": "Kilu Works",
|
|
||||||
"created_at": "2024-03-13T07:23:10.275174Z",
|
|
||||||
"icon": "😆"
|
|
||||||
},
|
|
||||||
"workspaces": [
|
|
||||||
{
|
|
||||||
"workspace_id": "81570fa8-8be9-4b2d-9f1c-1ef4f34079a8",
|
|
||||||
"database_storage_id": "6c1f1a2c-e8d5-4bc2-917f-495bce862abb",
|
|
||||||
"owner_uid": 311828434584080384,
|
|
||||||
"owner_name": "Zack Zi Xiang Fu",
|
|
||||||
"workspace_type": 0,
|
|
||||||
"workspace_name": "My Workspace",
|
|
||||||
"created_at": "2024-04-03T13:53:18.295918Z",
|
|
||||||
"icon": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"workspace_id": "fcb503f9-9287-4de4-8de0-ea191e680968",
|
|
||||||
"database_storage_id": "ae1b82a5-2b93-45c7-901a-f9357c544534",
|
|
||||||
"owner_uid": 276169796100296704,
|
|
||||||
"owner_name": "Annie Anqi Wang",
|
|
||||||
"workspace_type": 0,
|
|
||||||
"workspace_name": "AppFlowy Test",
|
|
||||||
"created_at": "2023-12-27T04:18:36.372013Z",
|
|
||||||
"icon": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"workspace_id": "9eebea03-3ed5-4298-86b2-a7f77856d48b",
|
|
||||||
"database_storage_id": "375874be-7a4f-4b7c-8b89-1dc9a39838f4",
|
|
||||||
"owner_uid": 304120109071339520,
|
|
||||||
"owner_name": "Kilu",
|
|
||||||
"workspace_type": 0,
|
|
||||||
"workspace_name": "Kilu Works",
|
|
||||||
"created_at": "2024-03-13T07:23:10.275174Z",
|
|
||||||
"icon": "😆"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"code": 0,
|
|
||||||
"message": "Operation completed successfully."
|
|
||||||
}
|
|
@ -25,9 +25,14 @@
|
|||||||
// -- This will overwrite an existing command --
|
// -- This will overwrite an existing command --
|
||||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||||
//
|
//
|
||||||
|
import { YDoc } from '@/application/collab.type';
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
import { JSDatabaseService } from '@/application/services/js-services/database.service';
|
||||||
|
import { JSDocumentService } from '@/application/services/js-services/document.service';
|
||||||
|
import { applyYDoc } from '@/application/ydoc/apply';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
Cypress.Commands.add('mockAPI', () => {
|
Cypress.Commands.add('mockAPI', () => {
|
||||||
cy.fixture('sign_in_success').then((json) => {
|
cy.fixture('sign_in_success').then((json) => {
|
||||||
cy.intercept('GET', `/api/user/verify/${json.access_token}`, {
|
cy.intercept('GET', `/api/user/verify/${json.access_token}`, {
|
||||||
@ -45,3 +50,71 @@ Cypress.Commands.add('mockAPI', () => {
|
|||||||
// cy.mockAPI();
|
// cy.mockAPI();
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
Cypress.Commands.add('mockCurrentWorkspace', () => {
|
||||||
|
cy.fixture('current_workspace').then((workspace) => {
|
||||||
|
cy.stub(JSDatabaseService.prototype, 'currentWorkspace').resolves(workspace);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
Cypress.Commands.add('mockGetWorkspaceDatabases', () => {
|
||||||
|
cy.fixture('database/databases').then((databases) => {
|
||||||
|
cy.stub(JSDatabaseService.prototype, 'getWorkspaceDatabases').resolves(databases);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
Cypress.Commands.add('mockDatabase', () => {
|
||||||
|
cy.mockCurrentWorkspace();
|
||||||
|
cy.mockGetWorkspaceDatabases();
|
||||||
|
|
||||||
|
const ids = [
|
||||||
|
'4c658817-20db-4f56-b7f9-0637a22dfeb6',
|
||||||
|
'ce267d12-3b61-4ebb-bb03-d65272f5f817',
|
||||||
|
'ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d',
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockOpenDatabase = cy.stub(JSDatabaseService.prototype, 'openDatabase');
|
||||||
|
|
||||||
|
ids.forEach((id) => {
|
||||||
|
cy.fixture(`database/${id}`).then((database) => {
|
||||||
|
cy.fixture(`database/rows/${id}`).then((rows) => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const rootRowsDoc = new Y.Doc();
|
||||||
|
const rowsFolder: Y.Map<YDoc> = rootRowsDoc.getMap();
|
||||||
|
const databaseState = new Uint8Array(database.data.doc_state);
|
||||||
|
|
||||||
|
applyYDoc(doc, databaseState);
|
||||||
|
|
||||||
|
Object.keys(rows).forEach((key) => {
|
||||||
|
const data = rows[key];
|
||||||
|
const rowDoc = new Y.Doc();
|
||||||
|
|
||||||
|
applyYDoc(rowDoc, new Uint8Array(data));
|
||||||
|
rowsFolder.set(key, rowDoc);
|
||||||
|
});
|
||||||
|
mockOpenDatabase.withArgs(id).resolves({
|
||||||
|
databaseDoc: doc,
|
||||||
|
rows: rowsFolder,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
Cypress.Commands.add('mockDocument', (id: string) => {
|
||||||
|
cy.fixture(`document/${id}`).then((subDocument) => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const state = new Uint8Array(subDocument.data.doc_state);
|
||||||
|
|
||||||
|
applyYDoc(doc, state);
|
||||||
|
|
||||||
|
cy.stub(JSDocumentService.prototype, 'openDocument').withArgs(id).resolves(doc);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import '@cypress/code-coverage/support';
|
||||||
import './commands';
|
import './commands';
|
||||||
import './document';
|
import './document';
|
||||||
// Alternatively you can use CommonJS syntax:
|
// Alternatively you can use CommonJS syntax:
|
||||||
@ -31,6 +32,10 @@ declare global {
|
|||||||
interface Chainable {
|
interface Chainable {
|
||||||
mount: typeof mount;
|
mount: typeof mount;
|
||||||
mockAPI: () => void;
|
mockAPI: () => void;
|
||||||
|
mockDatabase: () => void;
|
||||||
|
mockCurrentWorkspace: () => void;
|
||||||
|
mockGetWorkspaceDatabases: () => void;
|
||||||
|
mockDocument: (id: string) => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,3 +44,4 @@ Cypress.Commands.add('mount', mount);
|
|||||||
|
|
||||||
// Example use:
|
// Example use:
|
||||||
// cy.mount(<MyComponent />)
|
// cy.mount(<MyComponent />)
|
||||||
|
|
||||||
|
@ -17,4 +17,7 @@ module.exports = {
|
|||||||
'(.*)/node_modules/nanoid/.+\\.(j|t)sx?$': 'ts-jest',
|
'(.*)/node_modules/nanoid/.+\\.(j|t)sx?$': 'ts-jest',
|
||||||
},
|
},
|
||||||
'transformIgnorePatterns': [`/node_modules/(?!${esModules})`],
|
'transformIgnorePatterns': [`/node_modules/(?!${esModules})`],
|
||||||
|
testMatch: ['**/*.test.ts'],
|
||||||
|
coverageDirectory: '<rootDir>/coverage/jest',
|
||||||
|
collectCoverage: true,
|
||||||
};
|
};
|
@ -19,7 +19,10 @@
|
|||||||
"cypress:open": "cypress open",
|
"cypress:open": "cypress open",
|
||||||
"test": "pnpm run test:unit && pnpm run test:components",
|
"test": "pnpm run test:unit && pnpm run test:components",
|
||||||
"test:components": "cypress run --component --browser chrome --headless",
|
"test:components": "cypress run --component --browser chrome --headless",
|
||||||
"test:unit": "jest"
|
"test:unit": "jest --coverage",
|
||||||
|
"test:cy": "cypress run",
|
||||||
|
"merge-coverage": "node scripts/merge-coverage.cjs",
|
||||||
|
"coverage": "pnpm run test:unit && pnpm run test:components && pnpm run merge-coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@appflowyinc/client-api-wasm": "0.0.3",
|
"@appflowyinc/client-api-wasm": "0.0.3",
|
||||||
@ -86,6 +89,7 @@
|
|||||||
"slate": "^0.101.4",
|
"slate": "^0.101.4",
|
||||||
"slate-history": "^0.100.0",
|
"slate-history": "^0.100.0",
|
||||||
"slate-react": "^0.101.3",
|
"slate-react": "^0.101.3",
|
||||||
|
"smooth-scroll-into-view-if-needed": "^2.0.2",
|
||||||
"ts-results": "^3.3.0",
|
"ts-results": "^3.3.0",
|
||||||
"unsplash-js": "^7.0.19",
|
"unsplash-js": "^7.0.19",
|
||||||
"utf8": "^3.0.0",
|
"utf8": "^3.0.0",
|
||||||
@ -95,6 +99,9 @@
|
|||||||
"yjs": "^13.6.14"
|
"yjs": "^13.6.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@cypress/code-coverage": "^3.12.39",
|
||||||
|
"@istanbuljs/nyc-config-babel": "^3.0.0",
|
||||||
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||||
"@svgr/plugin-svgo": "^8.0.1",
|
"@svgr/plugin-svgo": "^8.0.1",
|
||||||
"@tauri-apps/cli": "^1.5.11",
|
"@tauri-apps/cli": "^1.5.11",
|
||||||
"@types/google-protobuf": "^3.15.12",
|
"@types/google-protobuf": "^3.15.12",
|
||||||
@ -132,7 +139,9 @@
|
|||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
|
"istanbul-lib-coverage": "^3.2.2",
|
||||||
"jest-environment-jsdom": "^29.6.2",
|
"jest-environment-jsdom": "^29.6.2",
|
||||||
|
"nyc": "^15.1.0",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"prettier": "2.8.4",
|
"prettier": "2.8.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||||
@ -147,6 +156,7 @@
|
|||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0",
|
||||||
"vite-plugin-compression2": "^1.0.0",
|
"vite-plugin-compression2": "^1.0.0",
|
||||||
"vite-plugin-importer": "^0.2.5",
|
"vite-plugin-importer": "^0.2.5",
|
||||||
|
"vite-plugin-istanbul": "^6.0.2",
|
||||||
"vite-plugin-svgr": "^3.2.0",
|
"vite-plugin-svgr": "^3.2.0",
|
||||||
"vite-plugin-terminal": "^1.2.0",
|
"vite-plugin-terminal": "^1.2.0",
|
||||||
"vite-plugin-total-bundle-size": "^1.0.7"
|
"vite-plugin-total-bundle-size": "^1.0.7"
|
||||||
|
31
frontend/appflowy_web_app/scripts/merge-coverage.cjs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
const { execSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const jestCoverageFile = path.join(__dirname, '../coverage/jest/coverage-final.json');
|
||||||
|
const cypressCoverageFile = path.join(__dirname, '../coverage/cypress/coverage-final.json');
|
||||||
|
// const cypressComponentCoverageFile = path.join(__dirname, '../coverage/cypress-component/coverage-final.json');
|
||||||
|
const nycOutputDir = path.join(__dirname, '../coverage/.nyc_output');
|
||||||
|
// Ensure .nyc_output directory exists
|
||||||
|
if (!fs.existsSync(nycOutputDir)) {
|
||||||
|
fs.mkdirSync(nycOutputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
// Copy Jest coverage file
|
||||||
|
fs.copyFileSync(jestCoverageFile, path.join(nycOutputDir, 'jest-coverage.json'));
|
||||||
|
// Copy Cypress E2E coverage file
|
||||||
|
fs.copyFileSync(cypressCoverageFile, path.join(nycOutputDir, 'cypress-coverage.json'));
|
||||||
|
// Copy Cypress Component coverage file
|
||||||
|
// fs.copyFileSync(cypressComponentCoverageFile, path.join(nycOutputDir, 'cypress-component-coverage.json'));
|
||||||
|
// Merge coverage files
|
||||||
|
execSync('nyc merge ./coverage/.nyc_output ./coverage/merged/coverage-final.json', { stdio: 'inherit' });
|
||||||
|
// Generate final merged report
|
||||||
|
execSync('nyc report --reporter=html --reporter=text-summary --report-dir=coverage/merged --temp-dir=coverage/.nyc_output', { stdio: 'inherit' });
|
||||||
|
console.log(`Merged coverage report written to coverage/merged`);
|
||||||
|
|
||||||
|
const GITHUB_STEP_SUMMARY = process.env.GITHUB_STEP_SUMMARY;
|
||||||
|
|
||||||
|
if (GITHUB_STEP_SUMMARY) {
|
||||||
|
const coverageSummary = execSync('nyc report --reporter=html --reporter=text-summary --report-dir=coverage/merged --temp-dir=coverage/.nyc_output').toString();
|
||||||
|
|
||||||
|
fs.appendFileSync(GITHUB_STEP_SUMMARY, `### Coverage Report\n\`\`\`\n${coverageSummary}\n\`\`\`\n`);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,661 @@
|
|||||||
|
import {
|
||||||
|
NumberFilterCondition,
|
||||||
|
TextFilterCondition,
|
||||||
|
CheckboxFilterCondition,
|
||||||
|
ChecklistFilterCondition,
|
||||||
|
SelectOptionFilterCondition,
|
||||||
|
Row,
|
||||||
|
} from '@/application/database-yjs';
|
||||||
|
import { withTestingData } from '@/application/database-yjs/__tests__/withTestingData';
|
||||||
|
import {
|
||||||
|
withCheckboxFilter,
|
||||||
|
withChecklistFilter,
|
||||||
|
withDateTimeFilter,
|
||||||
|
withMultiSelectOptionFilter,
|
||||||
|
withNumberFilter,
|
||||||
|
withRichTextFilter,
|
||||||
|
withSingleSelectOptionFilter,
|
||||||
|
withUrlFilter,
|
||||||
|
} from '@/application/database-yjs/__tests__/withTestingFilters';
|
||||||
|
import { withTestingRows } from '@/application/database-yjs/__tests__/withTestingRows';
|
||||||
|
import {
|
||||||
|
textFilterCheck,
|
||||||
|
numberFilterCheck,
|
||||||
|
checkboxFilterCheck,
|
||||||
|
checklistFilterCheck,
|
||||||
|
selectOptionFilterCheck,
|
||||||
|
filterBy,
|
||||||
|
} from '../filter';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
|
||||||
|
describe('Text filter check', () => {
|
||||||
|
const text = 'Hello, world!';
|
||||||
|
it('should return true for TextIs condition', () => {
|
||||||
|
const condition = TextFilterCondition.TextIs;
|
||||||
|
const content = 'Hello, world!';
|
||||||
|
|
||||||
|
const result = textFilterCheck(text, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for TextIs condition', () => {
|
||||||
|
const condition = TextFilterCondition.TextIs;
|
||||||
|
const content = 'Hello, world';
|
||||||
|
|
||||||
|
const result = textFilterCheck(text, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for TextIsNot condition', () => {
|
||||||
|
const condition = TextFilterCondition.TextIsNot;
|
||||||
|
const content = 'Hello, world';
|
||||||
|
|
||||||
|
const result = textFilterCheck(text, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for TextIsNot condition', () => {
|
||||||
|
const condition = TextFilterCondition.TextIsNot;
|
||||||
|
const content = 'Hello, world!';
|
||||||
|
|
||||||
|
const result = textFilterCheck(text, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for TextContains condition', () => {
|
||||||
|
const condition = TextFilterCondition.TextContains;
|
||||||
|
const content = 'world';
|
||||||
|
|
||||||
|
const result = textFilterCheck(text, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for TextContains condition', () => {
|
||||||
|
const condition = TextFilterCondition.TextContains;
|
||||||
|
const content = 'planet';
|
||||||
|
|
||||||
|
const result = textFilterCheck(text, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for TextDoesNotContain condition', () => {
|
||||||
|
const condition = TextFilterCondition.TextDoesNotContain;
|
||||||
|
const content = 'planet';
|
||||||
|
|
||||||
|
const result = textFilterCheck(text, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for TextDoesNotContain condition', () => {
|
||||||
|
const condition = TextFilterCondition.TextDoesNotContain;
|
||||||
|
const content = 'world';
|
||||||
|
|
||||||
|
const result = textFilterCheck(text, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for TextIsEmpty condition', () => {
|
||||||
|
const condition = TextFilterCondition.TextIsEmpty;
|
||||||
|
const text = '';
|
||||||
|
|
||||||
|
const result = textFilterCheck(text, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for TextIsEmpty condition', () => {
|
||||||
|
const condition = TextFilterCondition.TextIsEmpty;
|
||||||
|
const text = 'Hello, world!';
|
||||||
|
|
||||||
|
const result = textFilterCheck(text, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for TextIsNotEmpty condition', () => {
|
||||||
|
const condition = TextFilterCondition.TextIsNotEmpty;
|
||||||
|
const text = 'Hello, world!';
|
||||||
|
|
||||||
|
const result = textFilterCheck(text, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for TextIsNotEmpty condition', () => {
|
||||||
|
const condition = TextFilterCondition.TextIsNotEmpty;
|
||||||
|
const text = '';
|
||||||
|
|
||||||
|
const result = textFilterCheck(text, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for unknown condition', () => {
|
||||||
|
const condition = 42;
|
||||||
|
const content = 'Hello, world!';
|
||||||
|
|
||||||
|
const result = textFilterCheck(text, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Number filter check', () => {
|
||||||
|
const num = '42';
|
||||||
|
it('should return true for Equal condition', () => {
|
||||||
|
const condition = NumberFilterCondition.Equal;
|
||||||
|
const content = '42';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for Equal condition', () => {
|
||||||
|
const condition = NumberFilterCondition.Equal;
|
||||||
|
const content = '43';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for NotEqual condition', () => {
|
||||||
|
const condition = NumberFilterCondition.NotEqual;
|
||||||
|
const content = '43';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for NotEqual condition', () => {
|
||||||
|
const condition = NumberFilterCondition.NotEqual;
|
||||||
|
const content = '42';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for GreaterThan condition', () => {
|
||||||
|
const condition = NumberFilterCondition.GreaterThan;
|
||||||
|
const content = '41';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for GreaterThan condition', () => {
|
||||||
|
const condition = NumberFilterCondition.GreaterThan;
|
||||||
|
const content = '42';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for GreaterThanOrEqualTo condition', () => {
|
||||||
|
const condition = NumberFilterCondition.GreaterThanOrEqualTo;
|
||||||
|
const content = '42';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for GreaterThanOrEqualTo condition', () => {
|
||||||
|
const condition = NumberFilterCondition.GreaterThanOrEqualTo;
|
||||||
|
const content = '43';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for LessThan condition', () => {
|
||||||
|
const condition = NumberFilterCondition.LessThan;
|
||||||
|
const content = '43';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for LessThan condition', () => {
|
||||||
|
const condition = NumberFilterCondition.LessThan;
|
||||||
|
const content = '42';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for LessThanOrEqualTo condition', () => {
|
||||||
|
const condition = NumberFilterCondition.LessThanOrEqualTo;
|
||||||
|
const content = '42';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for LessThanOrEqualTo condition', () => {
|
||||||
|
const condition = NumberFilterCondition.LessThanOrEqualTo;
|
||||||
|
const content = '41';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for NumberIsEmpty condition', () => {
|
||||||
|
const condition = NumberFilterCondition.NumberIsEmpty;
|
||||||
|
|
||||||
|
const result = numberFilterCheck('', '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for NumberIsEmpty condition', () => {
|
||||||
|
const condition = NumberFilterCondition.NumberIsEmpty;
|
||||||
|
const num = '42';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for NumberIsNotEmpty condition', () => {
|
||||||
|
const condition = NumberFilterCondition.NumberIsNotEmpty;
|
||||||
|
const num = '42';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for NumberIsNotEmpty condition', () => {
|
||||||
|
const condition = NumberFilterCondition.NumberIsNotEmpty;
|
||||||
|
const num = '';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for unknown condition', () => {
|
||||||
|
const condition = 42;
|
||||||
|
const content = '42';
|
||||||
|
|
||||||
|
const result = numberFilterCheck(num, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Checkbox filter check', () => {
|
||||||
|
it('should return true for IsChecked condition', () => {
|
||||||
|
const condition = CheckboxFilterCondition.IsChecked;
|
||||||
|
const data = 'Yes';
|
||||||
|
|
||||||
|
const result = checkboxFilterCheck(data, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for IsChecked condition', () => {
|
||||||
|
const condition = CheckboxFilterCondition.IsChecked;
|
||||||
|
const data = 'No';
|
||||||
|
|
||||||
|
const result = checkboxFilterCheck(data, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for IsUnChecked condition', () => {
|
||||||
|
const condition = CheckboxFilterCondition.IsUnChecked;
|
||||||
|
const data = 'No';
|
||||||
|
|
||||||
|
const result = checkboxFilterCheck(data, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for IsUnChecked condition', () => {
|
||||||
|
const condition = CheckboxFilterCondition.IsUnChecked;
|
||||||
|
const data = 'Yes';
|
||||||
|
|
||||||
|
const result = checkboxFilterCheck(data, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for unknown condition', () => {
|
||||||
|
const condition = 42;
|
||||||
|
const data = 'Yes';
|
||||||
|
|
||||||
|
const result = checkboxFilterCheck(data, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Checklist filter check', () => {
|
||||||
|
it('should return true for IsComplete condition', () => {
|
||||||
|
const condition = ChecklistFilterCondition.IsComplete;
|
||||||
|
const data = JSON.stringify({
|
||||||
|
options: [
|
||||||
|
{ id: '1', name: 'Option 1' },
|
||||||
|
{ id: '2', name: 'Option 2' },
|
||||||
|
],
|
||||||
|
selected_option_ids: ['1', '2'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = checklistFilterCheck(data, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for IsComplete condition', () => {
|
||||||
|
const condition = ChecklistFilterCondition.IsComplete;
|
||||||
|
const data = JSON.stringify({
|
||||||
|
options: [
|
||||||
|
{ id: '1', name: 'Option 1' },
|
||||||
|
{ id: '2', name: 'Option 2' },
|
||||||
|
],
|
||||||
|
selected_option_ids: ['1'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = checklistFilterCheck(data, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for unknown condition', () => {
|
||||||
|
const condition = 42;
|
||||||
|
const data = JSON.stringify({
|
||||||
|
options: [
|
||||||
|
{ id: '1', name: 'Option 1' },
|
||||||
|
{ id: '2', name: 'Option 2' },
|
||||||
|
],
|
||||||
|
selected_option_ids: ['1', '2'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = checklistFilterCheck(data, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SelectOption filter check', () => {
|
||||||
|
it('should return true for OptionIs condition', () => {
|
||||||
|
const condition = SelectOptionFilterCondition.OptionIs;
|
||||||
|
const content = '1';
|
||||||
|
const data = '1,2';
|
||||||
|
|
||||||
|
const result = selectOptionFilterCheck(data, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for OptionIs condition', () => {
|
||||||
|
const condition = SelectOptionFilterCondition.OptionIs;
|
||||||
|
const content = '3';
|
||||||
|
const data = '1,2';
|
||||||
|
|
||||||
|
const result = selectOptionFilterCheck(data, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for OptionIsNot condition', () => {
|
||||||
|
const condition = SelectOptionFilterCondition.OptionIsNot;
|
||||||
|
const content = '3';
|
||||||
|
const data = '1,2';
|
||||||
|
|
||||||
|
const result = selectOptionFilterCheck(data, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for OptionIsNot condition', () => {
|
||||||
|
const condition = SelectOptionFilterCondition.OptionIsNot;
|
||||||
|
const content = '1';
|
||||||
|
const data = '1,2';
|
||||||
|
|
||||||
|
const result = selectOptionFilterCheck(data, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for OptionContains condition', () => {
|
||||||
|
const condition = SelectOptionFilterCondition.OptionContains;
|
||||||
|
const content = '1,3';
|
||||||
|
const data = '1,2,3';
|
||||||
|
|
||||||
|
const result = selectOptionFilterCheck(data, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for OptionContains condition', () => {
|
||||||
|
const condition = SelectOptionFilterCondition.OptionContains;
|
||||||
|
const content = '4';
|
||||||
|
const data = '1,2,3';
|
||||||
|
|
||||||
|
const result = selectOptionFilterCheck(data, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for OptionDoesNotContain condition', () => {
|
||||||
|
const condition = SelectOptionFilterCondition.OptionDoesNotContain;
|
||||||
|
const content = '4,5';
|
||||||
|
const data = '1,2,3';
|
||||||
|
|
||||||
|
const result = selectOptionFilterCheck(data, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for OptionDoesNotContain condition', () => {
|
||||||
|
const condition = SelectOptionFilterCondition.OptionDoesNotContain;
|
||||||
|
const content = '1,3';
|
||||||
|
const data = '1,2,3';
|
||||||
|
|
||||||
|
const result = selectOptionFilterCheck(data, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for OptionIsEmpty condition', () => {
|
||||||
|
const condition = SelectOptionFilterCondition.OptionIsEmpty;
|
||||||
|
const data = '';
|
||||||
|
|
||||||
|
const result = selectOptionFilterCheck(data, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for OptionIsEmpty condition', () => {
|
||||||
|
const condition = SelectOptionFilterCondition.OptionIsEmpty;
|
||||||
|
const data = '1,2';
|
||||||
|
|
||||||
|
const result = selectOptionFilterCheck(data, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for OptionIsNotEmpty condition', () => {
|
||||||
|
const condition = SelectOptionFilterCondition.OptionIsNotEmpty;
|
||||||
|
const data = '1,2';
|
||||||
|
|
||||||
|
const result = selectOptionFilterCheck(data, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for OptionIsNotEmpty condition', () => {
|
||||||
|
const condition = SelectOptionFilterCondition.OptionIsNotEmpty;
|
||||||
|
const data = '';
|
||||||
|
|
||||||
|
const result = selectOptionFilterCheck(data, '', condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for unknown condition', () => {
|
||||||
|
const condition = 42;
|
||||||
|
const content = '1';
|
||||||
|
const data = '1,2';
|
||||||
|
|
||||||
|
const result = selectOptionFilterCheck(data, content, condition);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Database filterBy', () => {
|
||||||
|
let rows: Row[];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
rows = withTestingRows();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all rows for empty filter', () => {
|
||||||
|
const { filters, fields, rowMap } = withTestingData();
|
||||||
|
const result = filterBy(rows, filters, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(result).toBe('1,2,3,4,5,6,7,8,9,10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return rows that match text filter', () => {
|
||||||
|
const { filters, fields, rowMap } = withTestingData();
|
||||||
|
const filter = withRichTextFilter();
|
||||||
|
filters.push([filter]);
|
||||||
|
const result = filterBy(rows, filters, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(result).toBe('1,5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return rows that match number filter', () => {
|
||||||
|
const { filters, fields, rowMap } = withTestingData();
|
||||||
|
const filter = withNumberFilter();
|
||||||
|
filters.push([filter]);
|
||||||
|
const result = filterBy(rows, filters, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(result).toBe('4,5,6,7,8,9,10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return rows that match checkbox filter', () => {
|
||||||
|
const { filters, fields, rowMap } = withTestingData();
|
||||||
|
const filter = withCheckboxFilter();
|
||||||
|
filters.push([filter]);
|
||||||
|
const result = filterBy(rows, filters, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(result).toBe('2,4,6,8,10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return rows that match checklist filter', () => {
|
||||||
|
const { filters, fields, rowMap } = withTestingData();
|
||||||
|
const filter = withChecklistFilter();
|
||||||
|
filters.push([filter]);
|
||||||
|
const result = filterBy(rows, filters, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(result).toBe('1,2,4,5,6,7,8,10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return rows that match multiple filters', () => {
|
||||||
|
const { filters, fields, rowMap } = withTestingData();
|
||||||
|
const filter1 = withRichTextFilter();
|
||||||
|
const filter2 = withNumberFilter();
|
||||||
|
filters.push([filter1, filter2]);
|
||||||
|
const result = filterBy(rows, filters, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(result).toBe('5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return rows that match url filter', () => {
|
||||||
|
const { filters, fields, rowMap } = withTestingData();
|
||||||
|
const filter = withUrlFilter();
|
||||||
|
filters.push([filter]);
|
||||||
|
const result = filterBy(rows, filters, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(result).toBe('4');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return rows that match date filter', () => {
|
||||||
|
const { filters, fields, rowMap } = withTestingData();
|
||||||
|
const filter = withDateTimeFilter();
|
||||||
|
filters.push([filter]);
|
||||||
|
const result = filterBy(rows, filters, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(result).toBe('1,2,3,4,5,6,7,8,9,10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return rows that match select option filter', () => {
|
||||||
|
const { filters, fields, rowMap } = withTestingData();
|
||||||
|
const filter = withSingleSelectOptionFilter();
|
||||||
|
filters.push([filter]);
|
||||||
|
const result = filterBy(rows, filters, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(result).toBe('2,5,8');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return rows that match multi select option filter', () => {
|
||||||
|
const { filters, fields, rowMap } = withTestingData();
|
||||||
|
const filter = withMultiSelectOptionFilter();
|
||||||
|
filters.push([filter]);
|
||||||
|
const result = filterBy(rows, filters, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(result).toBe('1,2,3,5,6,7,8,9');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return rows that match multiple filters', () => {
|
||||||
|
const { filters, fields, rowMap } = withTestingData();
|
||||||
|
const filter1 = withNumberFilter();
|
||||||
|
const filter2 = withChecklistFilter();
|
||||||
|
filters.push([filter1, filter2]);
|
||||||
|
const result = filterBy(rows, filters, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(result).toBe('4,5,6,7,8,10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for all filters', () => {
|
||||||
|
const { filters, fields, rowMap } = withTestingData();
|
||||||
|
const filter1 = withNumberFilter();
|
||||||
|
const filter2 = withChecklistFilter();
|
||||||
|
const filter3 = withRichTextFilter();
|
||||||
|
const filter4 = withCheckboxFilter();
|
||||||
|
const filter5 = withSingleSelectOptionFilter();
|
||||||
|
const filter6 = withMultiSelectOptionFilter();
|
||||||
|
const filter7 = withUrlFilter();
|
||||||
|
const filter8 = withDateTimeFilter();
|
||||||
|
filters.push([filter1, filter2, filter3, filter4, filter5, filter6, filter7, filter8]);
|
||||||
|
const result = filterBy(rows, filters, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"filter_text_field": {
|
||||||
|
"field_id": "text_field",
|
||||||
|
"condition": 2,
|
||||||
|
"content": "w"
|
||||||
|
},
|
||||||
|
"filter_number_field": {
|
||||||
|
"field_id": "number_field",
|
||||||
|
"condition": 2,
|
||||||
|
"content": 1000
|
||||||
|
},
|
||||||
|
"filter_date_field": {
|
||||||
|
"field_id": "date_field",
|
||||||
|
"condition": 1,
|
||||||
|
"content": 1685798400000
|
||||||
|
},
|
||||||
|
"filter_checkbox_field": {
|
||||||
|
"field_id": "checkbox_field",
|
||||||
|
"condition": 1
|
||||||
|
},
|
||||||
|
"filter_checklist_field": {
|
||||||
|
"field_id": "checklist_field",
|
||||||
|
"condition": 1
|
||||||
|
},
|
||||||
|
"filter_url_field": {
|
||||||
|
"field_id": "url_field",
|
||||||
|
"condition": 0,
|
||||||
|
"content": "https://example.com/4"
|
||||||
|
},
|
||||||
|
"filter_single_select_field": {
|
||||||
|
"field_id": "single_select_field",
|
||||||
|
"condition": 0,
|
||||||
|
"content": "2"
|
||||||
|
},
|
||||||
|
"filter_multi_select_field": {
|
||||||
|
"field_id": "multi_select_field",
|
||||||
|
"condition": 2,
|
||||||
|
"content": "1,3"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,412 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"cells": {
|
||||||
|
"text_field": {
|
||||||
|
"id": "text_field",
|
||||||
|
"data": "Hello world"
|
||||||
|
},
|
||||||
|
"number_field": {
|
||||||
|
"id": "number_field",
|
||||||
|
"data": 123
|
||||||
|
},
|
||||||
|
"checkbox_field": {
|
||||||
|
"id": "checkbox_field",
|
||||||
|
"data": "Yes"
|
||||||
|
},
|
||||||
|
"date_field": {
|
||||||
|
"id": "date_field",
|
||||||
|
"data": 1685539200000,
|
||||||
|
"end_timestamp": 1685625600000,
|
||||||
|
"include_time": true,
|
||||||
|
"is_range": false,
|
||||||
|
"reminder_id": "rem1"
|
||||||
|
},
|
||||||
|
"url_field": {
|
||||||
|
"id": "url_field",
|
||||||
|
"data": "https://example.com/1"
|
||||||
|
},
|
||||||
|
"single_select_field": {
|
||||||
|
"id": "single_select_field",
|
||||||
|
"data": "1"
|
||||||
|
},
|
||||||
|
"multi_select_field": {
|
||||||
|
"id": "multi_select_field",
|
||||||
|
"data": "1,2"
|
||||||
|
},
|
||||||
|
"checklist_field": {
|
||||||
|
"id": "checklist_field",
|
||||||
|
"data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"1\"]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"cells": {
|
||||||
|
"text_field": {
|
||||||
|
"id": "text_field",
|
||||||
|
"data": "Good morning"
|
||||||
|
},
|
||||||
|
"number_field": {
|
||||||
|
"id": "number_field",
|
||||||
|
"data": 456
|
||||||
|
},
|
||||||
|
"checkbox_field": {
|
||||||
|
"id": "checkbox_field",
|
||||||
|
"data": "No"
|
||||||
|
},
|
||||||
|
"date_field": {
|
||||||
|
"id": "date_field",
|
||||||
|
"data": 1685625600000,
|
||||||
|
"end_timestamp": 1685712000000,
|
||||||
|
"include_time": false,
|
||||||
|
"is_range": true,
|
||||||
|
"reminder_id": "rem2"
|
||||||
|
},
|
||||||
|
"url_field": {
|
||||||
|
"id": "url_field",
|
||||||
|
"data": "https://example.com/2"
|
||||||
|
},
|
||||||
|
"single_select_field": {
|
||||||
|
"id": "single_select_field",
|
||||||
|
"data": "2"
|
||||||
|
},
|
||||||
|
"multi_select_field": {
|
||||||
|
"id": "multi_select_field",
|
||||||
|
"data": "2,3"
|
||||||
|
},
|
||||||
|
"checklist_field": {
|
||||||
|
"id": "checklist_field",
|
||||||
|
"data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"2\"]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"cells": {
|
||||||
|
"text_field": {
|
||||||
|
"id": "text_field",
|
||||||
|
"data": "Good night"
|
||||||
|
},
|
||||||
|
"number_field": {
|
||||||
|
"id": "number_field",
|
||||||
|
"data": 789
|
||||||
|
},
|
||||||
|
"checkbox_field": {
|
||||||
|
"id": "checkbox_field",
|
||||||
|
"data": "Yes"
|
||||||
|
},
|
||||||
|
"date_field": {
|
||||||
|
"id": "date_field",
|
||||||
|
"data": 1685712000000,
|
||||||
|
"end_timestamp": 1685798400000,
|
||||||
|
"include_time": true,
|
||||||
|
"is_range": false,
|
||||||
|
"reminder_id": "rem3"
|
||||||
|
},
|
||||||
|
"url_field": {
|
||||||
|
"id": "url_field",
|
||||||
|
"data": "https://example.com/3"
|
||||||
|
},
|
||||||
|
"single_select_field": {
|
||||||
|
"id": "single_select_field",
|
||||||
|
"data": "3"
|
||||||
|
},
|
||||||
|
"multi_select_field": {
|
||||||
|
"id": "multi_select_field",
|
||||||
|
"data": "1,3"
|
||||||
|
},
|
||||||
|
"checklist_field": {
|
||||||
|
"id": "checklist_field",
|
||||||
|
"data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"1\",\"2\"]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"cells": {
|
||||||
|
"text_field": {
|
||||||
|
"id": "text_field",
|
||||||
|
"data": "Happy day"
|
||||||
|
},
|
||||||
|
"number_field": {
|
||||||
|
"id": "number_field",
|
||||||
|
"data": 1011
|
||||||
|
},
|
||||||
|
"checkbox_field": {
|
||||||
|
"id": "checkbox_field",
|
||||||
|
"data": "No"
|
||||||
|
},
|
||||||
|
"date_field": {
|
||||||
|
"id": "date_field",
|
||||||
|
"data": 1685798400000,
|
||||||
|
"end_timestamp": 1685884800000,
|
||||||
|
"include_time": false,
|
||||||
|
"is_range": true,
|
||||||
|
"reminder_id": "rem4"
|
||||||
|
},
|
||||||
|
"url_field": {
|
||||||
|
"id": "url_field",
|
||||||
|
"data": "https://example.com/4"
|
||||||
|
},
|
||||||
|
"single_select_field": {
|
||||||
|
"id": "single_select_field",
|
||||||
|
"data": "1"
|
||||||
|
},
|
||||||
|
"multi_select_field": {
|
||||||
|
"id": "multi_select_field",
|
||||||
|
"data": "2"
|
||||||
|
},
|
||||||
|
"checklist_field": {
|
||||||
|
"id": "checklist_field",
|
||||||
|
"data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"cells": {
|
||||||
|
"text_field": {
|
||||||
|
"id": "text_field",
|
||||||
|
"data": "Sunny weather"
|
||||||
|
},
|
||||||
|
"number_field": {
|
||||||
|
"id": "number_field",
|
||||||
|
"data": 1213
|
||||||
|
},
|
||||||
|
"checkbox_field": {
|
||||||
|
"id": "checkbox_field",
|
||||||
|
"data": "Yes"
|
||||||
|
},
|
||||||
|
"date_field": {
|
||||||
|
"id": "date_field",
|
||||||
|
"data": 1685884800000,
|
||||||
|
"end_timestamp": 1685971200000,
|
||||||
|
"include_time": true,
|
||||||
|
"is_range": false,
|
||||||
|
"reminder_id": "rem5"
|
||||||
|
},
|
||||||
|
"url_field": {
|
||||||
|
"id": "url_field",
|
||||||
|
"data": "https://example.com/5"
|
||||||
|
},
|
||||||
|
"single_select_field": {
|
||||||
|
"id": "single_select_field",
|
||||||
|
"data": "2"
|
||||||
|
},
|
||||||
|
"multi_select_field": {
|
||||||
|
"id": "multi_select_field",
|
||||||
|
"data": "1,2,3"
|
||||||
|
},
|
||||||
|
"checklist_field": {
|
||||||
|
"id": "checklist_field",
|
||||||
|
"data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"1\"]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6",
|
||||||
|
"cells": {
|
||||||
|
"text_field": {
|
||||||
|
"id": "text_field",
|
||||||
|
"data": "Rainy day"
|
||||||
|
},
|
||||||
|
"number_field": {
|
||||||
|
"id": "number_field",
|
||||||
|
"data": 1415
|
||||||
|
},
|
||||||
|
"checkbox_field": {
|
||||||
|
"id": "checkbox_field",
|
||||||
|
"data": "No"
|
||||||
|
},
|
||||||
|
"date_field": {
|
||||||
|
"id": "date_field",
|
||||||
|
"data": 1685971200000,
|
||||||
|
"end_timestamp": 1686057600000,
|
||||||
|
"include_time": false,
|
||||||
|
"is_range": true,
|
||||||
|
"reminder_id": "rem6"
|
||||||
|
},
|
||||||
|
"url_field": {
|
||||||
|
"id": "url_field",
|
||||||
|
"data": "https://example.com/6"
|
||||||
|
},
|
||||||
|
"single_select_field": {
|
||||||
|
"id": "single_select_field",
|
||||||
|
"data": "3"
|
||||||
|
},
|
||||||
|
"multi_select_field": {
|
||||||
|
"id": "multi_select_field",
|
||||||
|
"data": "1,3"
|
||||||
|
},
|
||||||
|
"checklist_field": {
|
||||||
|
"id": "checklist_field",
|
||||||
|
"data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"2\"]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7",
|
||||||
|
"cells": {
|
||||||
|
"text_field": {
|
||||||
|
"id": "text_field",
|
||||||
|
"data": "Winter is coming"
|
||||||
|
},
|
||||||
|
"number_field": {
|
||||||
|
"id": "number_field",
|
||||||
|
"data": 1617
|
||||||
|
},
|
||||||
|
"checkbox_field": {
|
||||||
|
"id": "checkbox_field",
|
||||||
|
"data": "Yes"
|
||||||
|
},
|
||||||
|
"date_field": {
|
||||||
|
"id": "date_field",
|
||||||
|
"data": 1686057600000,
|
||||||
|
"end_timestamp": 1686144000000,
|
||||||
|
"include_time": true,
|
||||||
|
"is_range": false,
|
||||||
|
"reminder_id": "rem7"
|
||||||
|
},
|
||||||
|
"url_field": {
|
||||||
|
"id": "url_field",
|
||||||
|
"data": "https://example.com/7"
|
||||||
|
},
|
||||||
|
"single_select_field": {
|
||||||
|
"id": "single_select_field",
|
||||||
|
"data": "1"
|
||||||
|
},
|
||||||
|
"multi_select_field": {
|
||||||
|
"id": "multi_select_field",
|
||||||
|
"data": "1,2"
|
||||||
|
},
|
||||||
|
"checklist_field": {
|
||||||
|
"id": "checklist_field",
|
||||||
|
"data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"1\"]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8",
|
||||||
|
"cells": {
|
||||||
|
"text_field": {
|
||||||
|
"id": "text_field",
|
||||||
|
"data": "Summer vibes"
|
||||||
|
},
|
||||||
|
"number_field": {
|
||||||
|
"id": "number_field",
|
||||||
|
"data": 1819
|
||||||
|
},
|
||||||
|
"checkbox_field": {
|
||||||
|
"id": "checkbox_field",
|
||||||
|
"data": "No"
|
||||||
|
},
|
||||||
|
"date_field": {
|
||||||
|
"id": "date_field",
|
||||||
|
"data": 1686144000000,
|
||||||
|
"end_timestamp": 1686230400000,
|
||||||
|
"include_time": false,
|
||||||
|
"is_range": true,
|
||||||
|
"reminder_id": "rem8"
|
||||||
|
},
|
||||||
|
"url_field": {
|
||||||
|
"id": "url_field",
|
||||||
|
"data": "https://example.com/8"
|
||||||
|
},
|
||||||
|
"single_select_field": {
|
||||||
|
"id": "single_select_field",
|
||||||
|
"data": "2"
|
||||||
|
},
|
||||||
|
"multi_select_field": {
|
||||||
|
"id": "multi_select_field",
|
||||||
|
"data": "2,3"
|
||||||
|
},
|
||||||
|
"checklist_field": {
|
||||||
|
"id": "checklist_field",
|
||||||
|
"data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"2\"]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9",
|
||||||
|
"cells": {
|
||||||
|
"text_field": {
|
||||||
|
"id": "text_field",
|
||||||
|
"data": "Autumn leaves"
|
||||||
|
},
|
||||||
|
"number_field": {
|
||||||
|
"id": "number_field",
|
||||||
|
"data": 2021
|
||||||
|
},
|
||||||
|
"checkbox_field": {
|
||||||
|
"id": "checkbox_field",
|
||||||
|
"data": "Yes"
|
||||||
|
},
|
||||||
|
"date_field": {
|
||||||
|
"id": "date_field",
|
||||||
|
"data": 1686230400000,
|
||||||
|
"end_timestamp": 1686316800000,
|
||||||
|
"include_time": true,
|
||||||
|
"is_range": false,
|
||||||
|
"reminder_id": "rem9"
|
||||||
|
},
|
||||||
|
"url_field": {
|
||||||
|
"id": "url_field",
|
||||||
|
"data": "https://example.com/9"
|
||||||
|
},
|
||||||
|
"single_select_field": {
|
||||||
|
"id": "single_select_field",
|
||||||
|
"data": "3"
|
||||||
|
},
|
||||||
|
"multi_select_field": {
|
||||||
|
"id": "multi_select_field",
|
||||||
|
"data": "1,3"
|
||||||
|
},
|
||||||
|
"checklist_field": {
|
||||||
|
"id": "checklist_field",
|
||||||
|
"data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"1\",\"2\"]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "10",
|
||||||
|
"cells": {
|
||||||
|
"text_field": {
|
||||||
|
"id": "text_field",
|
||||||
|
"data": "Spring blossoms"
|
||||||
|
},
|
||||||
|
"number_field": {
|
||||||
|
"id": "number_field",
|
||||||
|
"data": 2223
|
||||||
|
},
|
||||||
|
"checkbox_field": {
|
||||||
|
"id": "checkbox_field",
|
||||||
|
"data": "No"
|
||||||
|
},
|
||||||
|
"date_field": {
|
||||||
|
"id": "date_field",
|
||||||
|
"data": 1686316800000,
|
||||||
|
"end_timestamp": 1686403200000,
|
||||||
|
"include_time": false,
|
||||||
|
"is_range": true,
|
||||||
|
"reminder_id": "rem10"
|
||||||
|
},
|
||||||
|
"url_field": {
|
||||||
|
"id": "url_field",
|
||||||
|
"data": "https://example.com/10"
|
||||||
|
},
|
||||||
|
"single_select_field": {
|
||||||
|
"id": "single_select_field",
|
||||||
|
"data": "1"
|
||||||
|
},
|
||||||
|
"multi_select_field": {
|
||||||
|
"id": "multi_select_field",
|
||||||
|
"data": "2"
|
||||||
|
},
|
||||||
|
"checklist_field": {
|
||||||
|
"id": "checklist_field",
|
||||||
|
"data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"sort_asc_text_field": {
|
||||||
|
"id": "sort_asc_text_field",
|
||||||
|
"field_id": "text_field",
|
||||||
|
"condition": "asc"
|
||||||
|
},
|
||||||
|
"sort_desc_text_field": {
|
||||||
|
"field_id": "text_field",
|
||||||
|
"condition": "desc",
|
||||||
|
"id": "sort_desc_text_field"
|
||||||
|
},
|
||||||
|
"sort_asc_number_field": {
|
||||||
|
"field_id": "number_field",
|
||||||
|
"condition": "asc",
|
||||||
|
"id": "sort_asc_number_field"
|
||||||
|
},
|
||||||
|
"sort_desc_number_field": {
|
||||||
|
"field_id": "number_field",
|
||||||
|
"condition": "desc",
|
||||||
|
"id": "sort_desc_number_field"
|
||||||
|
},
|
||||||
|
"sort_asc_date_field": {
|
||||||
|
"field_id": "date_field",
|
||||||
|
"condition": "asc",
|
||||||
|
"id": "sort_asc_date_field"
|
||||||
|
},
|
||||||
|
"sort_desc_date_field": {
|
||||||
|
"field_id": "date_field",
|
||||||
|
"condition": "desc",
|
||||||
|
"id": "sort_desc_date_field"
|
||||||
|
},
|
||||||
|
"sort_asc_checkbox_field": {
|
||||||
|
"field_id": "checkbox_field",
|
||||||
|
"condition": "asc",
|
||||||
|
"id": "sort_asc_checkbox_field"
|
||||||
|
},
|
||||||
|
"sort_desc_checkbox_field": {
|
||||||
|
"field_id": "checkbox_field",
|
||||||
|
"condition": "desc",
|
||||||
|
"id": "sort_desc_checkbox_field"
|
||||||
|
},
|
||||||
|
"sort_asc_checklist_field": {
|
||||||
|
"field_id": "checklist_field",
|
||||||
|
"condition": "asc",
|
||||||
|
"id": "sort_asc_checklist_field"
|
||||||
|
},
|
||||||
|
"sort_desc_checklist_field": {
|
||||||
|
"field_id": "checklist_field",
|
||||||
|
"condition": "desc",
|
||||||
|
"id": "sort_desc_checklist_field"
|
||||||
|
},
|
||||||
|
"sort_asc_single_select_field": {
|
||||||
|
"field_id": "single_select_field",
|
||||||
|
"condition": "asc",
|
||||||
|
"id": "sort_asc_single_select_field"
|
||||||
|
},
|
||||||
|
"sort_desc_single_select_field": {
|
||||||
|
"field_id": "single_select_field",
|
||||||
|
"condition": "desc",
|
||||||
|
"id": "sort_desc_single_select_field"
|
||||||
|
},
|
||||||
|
"sort_asc_multi_select_field": {
|
||||||
|
"field_id": "multi_select_field",
|
||||||
|
"condition": "asc",
|
||||||
|
"id": "sort_asc_multi_select_field"
|
||||||
|
},
|
||||||
|
"sort_desc_multi_select_field": {
|
||||||
|
"field_id": "multi_select_field",
|
||||||
|
"condition": "desc",
|
||||||
|
"id": "sort_desc_multi_select_field"
|
||||||
|
},
|
||||||
|
"sort_asc_url_field": {
|
||||||
|
"field_id": "url_field",
|
||||||
|
"condition": "asc",
|
||||||
|
"id": "sort_asc_url_field"
|
||||||
|
},
|
||||||
|
"sort_desc_url_field": {
|
||||||
|
"field_id": "url_field",
|
||||||
|
"condition": "desc",
|
||||||
|
"id": "sort_desc_url_field"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
import { Row } from '@/application/database-yjs';
|
||||||
|
import { withTestingData } from '@/application/database-yjs/__tests__/withTestingData';
|
||||||
|
import { withTestingRows } from '@/application/database-yjs/__tests__/withTestingRows';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
import { groupByField } from '../group';
|
||||||
|
|
||||||
|
describe('Database group', () => {
|
||||||
|
let rows: Row[];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
rows = withTestingRows();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if field is not select option', () => {
|
||||||
|
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 group by select option field', () => {
|
||||||
|
const { fields, rowMap } = withTestingData();
|
||||||
|
const field = fields.get('single_select_field');
|
||||||
|
const result = groupByField(rows, rowMap, field);
|
||||||
|
const expectRes = new Map([
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
[
|
||||||
|
{ id: '1', height: 37 },
|
||||||
|
{ id: '4', height: 37 },
|
||||||
|
{ id: '7', height: 37 },
|
||||||
|
{ id: '10', height: 37 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'2',
|
||||||
|
[
|
||||||
|
{ id: '2', height: 37 },
|
||||||
|
{ id: '5', height: 37 },
|
||||||
|
{ id: '8', height: 37 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'3',
|
||||||
|
[
|
||||||
|
{ id: '3', height: 37 },
|
||||||
|
{ id: '6', height: 37 },
|
||||||
|
{ id: '9', height: 37 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
expect(result).toEqual(expectRes);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should group by multi select option field', () => {
|
||||||
|
const { fields, rowMap } = withTestingData();
|
||||||
|
const field = fields.get('multi_select_field');
|
||||||
|
const result = groupByField(rows, rowMap, field);
|
||||||
|
const expectRes = new Map([
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
[
|
||||||
|
{ id: '1', height: 37 },
|
||||||
|
{ id: '3', height: 37 },
|
||||||
|
{ id: '5', height: 37 },
|
||||||
|
{ id: '6', height: 37 },
|
||||||
|
{ id: '7', height: 37 },
|
||||||
|
{ id: '9', height: 37 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'2',
|
||||||
|
[
|
||||||
|
{ id: '1', height: 37 },
|
||||||
|
{ id: '2', height: 37 },
|
||||||
|
{ id: '4', height: 37 },
|
||||||
|
{ id: '5', height: 37 },
|
||||||
|
{ id: '7', height: 37 },
|
||||||
|
{ id: '8', height: 37 },
|
||||||
|
{ id: '10', height: 37 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'3',
|
||||||
|
[
|
||||||
|
{ id: '2', height: 37 },
|
||||||
|
{ id: '3', height: 37 },
|
||||||
|
{ id: '5', height: 37 },
|
||||||
|
{ id: '6', height: 37 },
|
||||||
|
{ id: '8', height: 37 },
|
||||||
|
{ id: '9', height: 37 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
expect(result).toEqual(expectRes);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,314 @@
|
|||||||
|
import { Row } from '@/application/database-yjs';
|
||||||
|
import { withTestingData } from '@/application/database-yjs/__tests__/withTestingData';
|
||||||
|
import { withTestingRows } from '@/application/database-yjs/__tests__/withTestingRows';
|
||||||
|
import {
|
||||||
|
withCheckboxSort,
|
||||||
|
withChecklistSort,
|
||||||
|
withDateTimeSort,
|
||||||
|
withMultiSelectOptionSort,
|
||||||
|
withNumberSort,
|
||||||
|
withRichTextSort,
|
||||||
|
withSingleSelectOptionSort,
|
||||||
|
withUrlSort,
|
||||||
|
} from '@/application/database-yjs/__tests__/withTestingSorts';
|
||||||
|
import {
|
||||||
|
withCheckboxTestingField,
|
||||||
|
withDateTimeTestingField,
|
||||||
|
withNumberTestingField,
|
||||||
|
withRichTextTestingField,
|
||||||
|
withSelectOptionTestingField,
|
||||||
|
withURLTestingField,
|
||||||
|
withChecklistTestingField,
|
||||||
|
} from './withTestingField';
|
||||||
|
import { sortBy, parseCellDataForSort } from '../sort';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
|
||||||
|
describe('parseCellDataForSort', () => {
|
||||||
|
it('should parse data correctly based on field type', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const field = withNumberTestingField();
|
||||||
|
doc.getMap().set('field', field);
|
||||||
|
const data = 42;
|
||||||
|
|
||||||
|
const result = parseCellDataForSort(field, data);
|
||||||
|
|
||||||
|
expect(result).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default value for empty rich text', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const field = withRichTextTestingField();
|
||||||
|
doc.getMap().set('field', field);
|
||||||
|
const data = '';
|
||||||
|
|
||||||
|
const result = parseCellDataForSort(field, data);
|
||||||
|
|
||||||
|
expect(result).toEqual('\uFFFF');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default value for empty URL', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const field = withURLTestingField();
|
||||||
|
doc.getMap().set('field', field);
|
||||||
|
const data = '';
|
||||||
|
|
||||||
|
const result = parseCellDataForSort(field, data);
|
||||||
|
|
||||||
|
expect(result).toBe('\uFFFF');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return data for non-empty rich text', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const field = withRichTextTestingField();
|
||||||
|
doc.getMap().set('field', field);
|
||||||
|
const data = 'Hello, world!';
|
||||||
|
|
||||||
|
const result = parseCellDataForSort(field, data);
|
||||||
|
|
||||||
|
expect(result).toBe(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse checkbox data correctly', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const field = withCheckboxTestingField();
|
||||||
|
doc.getMap().set('field', field);
|
||||||
|
const data = 'Yes';
|
||||||
|
|
||||||
|
const result = parseCellDataForSort(field, data);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
const noData = 'No';
|
||||||
|
const noResult = parseCellDataForSort(field, noData);
|
||||||
|
expect(noResult).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse DateTime data correctly', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const field = withDateTimeTestingField();
|
||||||
|
doc.getMap().set('field', field);
|
||||||
|
const data = '1633046400000';
|
||||||
|
|
||||||
|
const result = parseCellDataForSort(field, data);
|
||||||
|
|
||||||
|
expect(result).toBe(Number(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse SingleSelect data correctly', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const field = withSelectOptionTestingField();
|
||||||
|
doc.getMap().set('field', field);
|
||||||
|
const data = '1';
|
||||||
|
|
||||||
|
const result = parseCellDataForSort(field, data);
|
||||||
|
|
||||||
|
expect(result).toBe('Option 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse MultiSelect data correctly', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const field = withSelectOptionTestingField();
|
||||||
|
doc.getMap().set('field', field);
|
||||||
|
const data = '1,2';
|
||||||
|
|
||||||
|
const result = parseCellDataForSort(field, data);
|
||||||
|
|
||||||
|
expect(result).toBe('Option 1, Option 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse Checklist data correctly', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const field = withChecklistTestingField();
|
||||||
|
doc.getMap().set('field', field);
|
||||||
|
const data = '[]';
|
||||||
|
|
||||||
|
const result = parseCellDataForSort(field, data);
|
||||||
|
|
||||||
|
expect(result).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Database sortBy', () => {
|
||||||
|
let rows: Row[];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
rows = withTestingRows();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by number field in ascending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withNumberSort();
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('1,2,3,4,5,6,7,8,9,10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by number field in descending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withNumberSort(false);
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('10,9,8,7,6,5,4,3,2,1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by rich text field in ascending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withRichTextSort();
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('9,2,3,4,1,6,10,8,5,7');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by rich text field in descending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withRichTextSort(false);
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('7,5,8,10,6,1,4,3,2,9');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by url field in ascending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withUrlSort();
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('1,10,2,3,4,5,6,7,8,9');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by url field in descending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withUrlSort(false);
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('9,8,7,6,5,4,3,2,10,1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by checkbox field in ascending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withCheckboxSort();
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('2,4,6,8,10,1,3,5,7,9');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by checkbox field in descending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withCheckboxSort(false);
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('1,3,5,7,9,2,4,6,8,10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by DateTime field in ascending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withDateTimeSort();
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('1,2,3,4,5,6,7,8,9,10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by DateTime field in descending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withDateTimeSort(false);
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('10,9,8,7,6,5,4,3,2,1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by SingleSelect field in ascending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withSingleSelectOptionSort();
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('1,4,7,10,2,5,8,3,6,9');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by SingleSelect field in descending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withSingleSelectOptionSort(false);
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('3,6,9,2,5,8,1,4,7,10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by MultiSelect field in ascending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withMultiSelectOptionSort();
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('1,7,5,3,6,9,4,10,2,8');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by MultiSelect field in descending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withMultiSelectOptionSort(false);
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('2,8,4,10,3,6,9,5,1,7');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by Checklist field in ascending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withChecklistSort();
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('4,10,1,2,5,6,7,8,3,9');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by Checklist field in descending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withChecklistSort(false);
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const sortedRows = sortBy(rows, sorts, fields, rowMap)
|
||||||
|
.map((row) => row.id)
|
||||||
|
.join(',');
|
||||||
|
expect(sortedRows).toBe('3,9,1,2,5,6,7,8,4,10');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,31 @@
|
|||||||
|
import { YDatabaseFields, YDatabaseFilters, YDatabaseSorts } from '@/application/collab.type';
|
||||||
|
import { withTestingFields } from '@/application/database-yjs/__tests__/withTestingField';
|
||||||
|
import { withTestingRowDataMap } from '@/application/database-yjs/__tests__/withTestingRows';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
|
export function withTestingData() {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const sharedRoot = doc.getMap();
|
||||||
|
const fields = withTestingFields() as YDatabaseFields;
|
||||||
|
|
||||||
|
sharedRoot.set('fields', fields);
|
||||||
|
|
||||||
|
const rowMap = withTestingRowDataMap();
|
||||||
|
|
||||||
|
sharedRoot.set('rows', rowMap);
|
||||||
|
|
||||||
|
const sorts = new Y.Array() as YDatabaseSorts;
|
||||||
|
|
||||||
|
sharedRoot.set('sorts', sorts);
|
||||||
|
|
||||||
|
const filters = new Y.Array() as YDatabaseFilters;
|
||||||
|
|
||||||
|
sharedRoot.set('filters', filters);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fields,
|
||||||
|
rowMap,
|
||||||
|
sorts,
|
||||||
|
filters,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,153 @@
|
|||||||
|
import {
|
||||||
|
YDatabaseField,
|
||||||
|
YDatabaseFieldTypeOption,
|
||||||
|
YjsDatabaseKey,
|
||||||
|
YMapFieldTypeOption,
|
||||||
|
} from '@/application/collab.type';
|
||||||
|
import { FieldType, SelectOptionColor } from '@/application/database-yjs';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
|
export function withTestingFields() {
|
||||||
|
const fields = new Y.Map();
|
||||||
|
const textField = withRichTextTestingField();
|
||||||
|
|
||||||
|
fields.set('text_field', textField);
|
||||||
|
const numberField = withNumberTestingField();
|
||||||
|
|
||||||
|
fields.set('number_field', numberField);
|
||||||
|
|
||||||
|
const checkboxField = withCheckboxTestingField();
|
||||||
|
|
||||||
|
fields.set('checkbox_field', checkboxField);
|
||||||
|
|
||||||
|
const dateTimeField = withDateTimeTestingField();
|
||||||
|
|
||||||
|
fields.set('date_field', dateTimeField);
|
||||||
|
|
||||||
|
const singleSelectField = withSelectOptionTestingField();
|
||||||
|
|
||||||
|
fields.set('single_select_field', singleSelectField);
|
||||||
|
const multipleSelectField = withSelectOptionTestingField(true);
|
||||||
|
|
||||||
|
fields.set('multi_select_field', multipleSelectField);
|
||||||
|
|
||||||
|
const urlField = withURLTestingField();
|
||||||
|
|
||||||
|
fields.set('url_field', urlField);
|
||||||
|
|
||||||
|
const checklistField = withChecklistTestingField();
|
||||||
|
|
||||||
|
fields.set('checklist_field', checklistField);
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withRichTextTestingField() {
|
||||||
|
const field = new Y.Map() as YDatabaseField;
|
||||||
|
const now = Date.now().toString();
|
||||||
|
|
||||||
|
field.set(YjsDatabaseKey.name, 'Rich Text Field');
|
||||||
|
field.set(YjsDatabaseKey.id, 'text_field');
|
||||||
|
field.set(YjsDatabaseKey.type, String(FieldType.RichText));
|
||||||
|
field.set(YjsDatabaseKey.last_modified, now.valueOf());
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withNumberTestingField() {
|
||||||
|
const field = new Y.Map() as YDatabaseField;
|
||||||
|
|
||||||
|
const now = Date.now().toString();
|
||||||
|
|
||||||
|
field.set(YjsDatabaseKey.name, 'Number Field');
|
||||||
|
field.set(YjsDatabaseKey.id, 'number_field');
|
||||||
|
field.set(YjsDatabaseKey.type, String(FieldType.Number));
|
||||||
|
field.set(YjsDatabaseKey.last_modified, now.valueOf());
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withCheckboxTestingField() {
|
||||||
|
const field = new Y.Map() as YDatabaseField;
|
||||||
|
const now = Date.now().toString();
|
||||||
|
|
||||||
|
field.set(YjsDatabaseKey.name, 'Checkbox Field');
|
||||||
|
field.set(YjsDatabaseKey.id, 'checkbox_field');
|
||||||
|
field.set(YjsDatabaseKey.type, String(FieldType.Checkbox));
|
||||||
|
field.set(YjsDatabaseKey.last_modified, now.valueOf());
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withDateTimeTestingField() {
|
||||||
|
const field = new Y.Map() as YDatabaseField;
|
||||||
|
const typeOption = new Y.Map() as YDatabaseFieldTypeOption;
|
||||||
|
const now = Date.now().toString();
|
||||||
|
|
||||||
|
field.set(YjsDatabaseKey.name, 'DateTime Field');
|
||||||
|
field.set(YjsDatabaseKey.id, 'date_field');
|
||||||
|
field.set(YjsDatabaseKey.type, String(FieldType.DateTime));
|
||||||
|
field.set(YjsDatabaseKey.last_modified, now.valueOf());
|
||||||
|
field.set(YjsDatabaseKey.type_option, typeOption);
|
||||||
|
|
||||||
|
const dateTypeOption = new Y.Map() as YMapFieldTypeOption;
|
||||||
|
|
||||||
|
typeOption.set(String(FieldType.DateTime), dateTypeOption);
|
||||||
|
|
||||||
|
dateTypeOption.set(YjsDatabaseKey.time_format, '0');
|
||||||
|
dateTypeOption.set(YjsDatabaseKey.date_format, '0');
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withURLTestingField() {
|
||||||
|
const field = new Y.Map() as YDatabaseField;
|
||||||
|
const now = Date.now().toString();
|
||||||
|
|
||||||
|
field.set(YjsDatabaseKey.name, 'URL Field');
|
||||||
|
field.set(YjsDatabaseKey.id, 'url_field');
|
||||||
|
field.set(YjsDatabaseKey.type, String(FieldType.URL));
|
||||||
|
field.set(YjsDatabaseKey.last_modified, now.valueOf());
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withSelectOptionTestingField(isMultiple = false) {
|
||||||
|
const field = new Y.Map() as YDatabaseField;
|
||||||
|
const typeOption = new Y.Map() as YDatabaseFieldTypeOption;
|
||||||
|
const now = Date.now().toString();
|
||||||
|
|
||||||
|
field.set(YjsDatabaseKey.name, 'Single Select Field');
|
||||||
|
field.set(YjsDatabaseKey.id, isMultiple ? 'multi_select_field' : 'single_select_field');
|
||||||
|
field.set(YjsDatabaseKey.type, String(FieldType.SingleSelect));
|
||||||
|
field.set(YjsDatabaseKey.last_modified, now.valueOf());
|
||||||
|
field.set(YjsDatabaseKey.type_option, typeOption);
|
||||||
|
|
||||||
|
const selectTypeOption = new Y.Map() as YMapFieldTypeOption;
|
||||||
|
|
||||||
|
typeOption.set(String(FieldType.SingleSelect), selectTypeOption);
|
||||||
|
|
||||||
|
selectTypeOption.set(
|
||||||
|
YjsDatabaseKey.content,
|
||||||
|
JSON.stringify({
|
||||||
|
disable_color: false,
|
||||||
|
options: [
|
||||||
|
{ id: '1', name: 'Option 1', color: SelectOptionColor.Purple },
|
||||||
|
{ id: '2', name: 'Option 2', color: SelectOptionColor.Pink },
|
||||||
|
{ id: '3', name: 'Option 3', color: SelectOptionColor.LightPink },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withChecklistTestingField() {
|
||||||
|
const field = new Y.Map() as YDatabaseField;
|
||||||
|
const now = Date.now().toString();
|
||||||
|
|
||||||
|
field.set(YjsDatabaseKey.name, 'Checklist Field');
|
||||||
|
field.set(YjsDatabaseKey.id, 'checklist_field');
|
||||||
|
field.set(YjsDatabaseKey.type, String(FieldType.Checklist));
|
||||||
|
field.set(YjsDatabaseKey.last_modified, now.valueOf());
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
import { YDatabaseFilter, YjsDatabaseKey } from '@/application/collab.type';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import * as filtersJson from './fixtures/filters.json';
|
||||||
|
|
||||||
|
export function withRichTextFilter() {
|
||||||
|
const filter = new Y.Map() as YDatabaseFilter;
|
||||||
|
|
||||||
|
filter.set(YjsDatabaseKey.id, 'filter_text_field');
|
||||||
|
filter.set(YjsDatabaseKey.field_id, filtersJson.filter_text_field.field_id);
|
||||||
|
filter.set(YjsDatabaseKey.condition, filtersJson.filter_text_field.condition);
|
||||||
|
filter.set(YjsDatabaseKey.content, filtersJson.filter_text_field.content);
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withUrlFilter() {
|
||||||
|
const filter = new Y.Map() as YDatabaseFilter;
|
||||||
|
|
||||||
|
filter.set(YjsDatabaseKey.id, 'filter_url_field');
|
||||||
|
filter.set(YjsDatabaseKey.field_id, filtersJson.filter_url_field.field_id);
|
||||||
|
filter.set(YjsDatabaseKey.condition, filtersJson.filter_url_field.condition);
|
||||||
|
filter.set(YjsDatabaseKey.content, filtersJson.filter_url_field.content);
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withNumberFilter() {
|
||||||
|
const filter = new Y.Map() as YDatabaseFilter;
|
||||||
|
|
||||||
|
filter.set(YjsDatabaseKey.id, 'filter_number_field');
|
||||||
|
filter.set(YjsDatabaseKey.field_id, filtersJson.filter_number_field.field_id);
|
||||||
|
filter.set(YjsDatabaseKey.condition, filtersJson.filter_number_field.condition);
|
||||||
|
filter.set(YjsDatabaseKey.content, filtersJson.filter_number_field.content);
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withCheckboxFilter() {
|
||||||
|
const filter = new Y.Map() as YDatabaseFilter;
|
||||||
|
|
||||||
|
filter.set(YjsDatabaseKey.id, 'filter_checkbox_field');
|
||||||
|
filter.set(YjsDatabaseKey.field_id, filtersJson.filter_checkbox_field.field_id);
|
||||||
|
filter.set(YjsDatabaseKey.condition, filtersJson.filter_checkbox_field.condition);
|
||||||
|
filter.set(YjsDatabaseKey.content, '');
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withChecklistFilter() {
|
||||||
|
const filter = new Y.Map() as YDatabaseFilter;
|
||||||
|
|
||||||
|
filter.set(YjsDatabaseKey.id, 'filter_checklist_field');
|
||||||
|
filter.set(YjsDatabaseKey.field_id, filtersJson.filter_checklist_field.field_id);
|
||||||
|
filter.set(YjsDatabaseKey.condition, filtersJson.filter_checklist_field.condition);
|
||||||
|
filter.set(YjsDatabaseKey.content, '');
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withSingleSelectOptionFilter() {
|
||||||
|
const filter = new Y.Map() as YDatabaseFilter;
|
||||||
|
|
||||||
|
filter.set(YjsDatabaseKey.id, 'filter_single_select_field');
|
||||||
|
filter.set(YjsDatabaseKey.field_id, filtersJson.filter_single_select_field.field_id);
|
||||||
|
filter.set(YjsDatabaseKey.condition, filtersJson.filter_single_select_field.condition);
|
||||||
|
filter.set(YjsDatabaseKey.content, filtersJson.filter_single_select_field.content);
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withMultiSelectOptionFilter() {
|
||||||
|
const filter = new Y.Map() as YDatabaseFilter;
|
||||||
|
|
||||||
|
filter.set(YjsDatabaseKey.id, 'filter_multi_select_field');
|
||||||
|
filter.set(YjsDatabaseKey.field_id, filtersJson.filter_multi_select_field.field_id);
|
||||||
|
filter.set(YjsDatabaseKey.condition, filtersJson.filter_multi_select_field.condition);
|
||||||
|
filter.set(YjsDatabaseKey.content, filtersJson.filter_multi_select_field.content);
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withDateTimeFilter() {
|
||||||
|
const filter = new Y.Map() as YDatabaseFilter;
|
||||||
|
|
||||||
|
filter.set(YjsDatabaseKey.id, 'filter_date_field');
|
||||||
|
filter.set(YjsDatabaseKey.field_id, filtersJson.filter_date_field.field_id);
|
||||||
|
filter.set(YjsDatabaseKey.condition, filtersJson.filter_date_field.condition);
|
||||||
|
filter.set(YjsDatabaseKey.content, filtersJson.filter_date_field.content);
|
||||||
|
return filter;
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
import {
|
||||||
|
YDatabaseCell,
|
||||||
|
YDatabaseCells,
|
||||||
|
YDatabaseRow,
|
||||||
|
YDoc,
|
||||||
|
YjsDatabaseKey,
|
||||||
|
YjsEditorKey,
|
||||||
|
} from '@/application/collab.type';
|
||||||
|
import { FieldType, Row } from '@/application/database-yjs';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import * as rowsJson from './fixtures/rows.json';
|
||||||
|
|
||||||
|
export function withTestingRows(): Row[] {
|
||||||
|
return rowsJson.map((row) => {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
height: 37,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTestingRowDataMap(): Y.Map<YDoc> {
|
||||||
|
const folder = new Y.Map();
|
||||||
|
const rows = withTestingRows();
|
||||||
|
|
||||||
|
rows.forEach((row, index) => {
|
||||||
|
const rowDoc = new Y.Doc();
|
||||||
|
const rowData = withTestingRowData(row.id, index);
|
||||||
|
|
||||||
|
rowDoc.getMap(YjsEditorKey.data_section).set(YjsEditorKey.database_row, rowData);
|
||||||
|
folder.set(row.id, rowDoc);
|
||||||
|
});
|
||||||
|
|
||||||
|
return folder as Y.Map<YDoc>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTestingRowData(id: string, index: number) {
|
||||||
|
const rowData = new Y.Map() as YDatabaseRow;
|
||||||
|
|
||||||
|
rowData.set(YjsDatabaseKey.id, id);
|
||||||
|
rowData.set(YjsDatabaseKey.height, 37);
|
||||||
|
|
||||||
|
const cells = new Y.Map() as YDatabaseCells;
|
||||||
|
|
||||||
|
const textFieldCell = withTestingCell(rowsJson[index].cells.text_field.data);
|
||||||
|
|
||||||
|
textFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.RichText));
|
||||||
|
cells.set('text_field', textFieldCell);
|
||||||
|
|
||||||
|
const numberFieldCell = withTestingCell(rowsJson[index].cells.number_field.data);
|
||||||
|
|
||||||
|
numberFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.Number));
|
||||||
|
cells.set('number_field', numberFieldCell);
|
||||||
|
|
||||||
|
const checkboxFieldCell = withTestingCell(rowsJson[index].cells.checkbox_field.data);
|
||||||
|
|
||||||
|
checkboxFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.Checkbox));
|
||||||
|
cells.set('checkbox_field', checkboxFieldCell);
|
||||||
|
|
||||||
|
const dateTimeFieldCell = withTestingCell(rowsJson[index].cells.date_field.data);
|
||||||
|
|
||||||
|
dateTimeFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.DateTime));
|
||||||
|
cells.set('date_field', dateTimeFieldCell);
|
||||||
|
|
||||||
|
const urlFieldCell = withTestingCell(rowsJson[index].cells.url_field.data);
|
||||||
|
|
||||||
|
urlFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.URL));
|
||||||
|
cells.set('url_field', urlFieldCell);
|
||||||
|
|
||||||
|
const singleSelectFieldCell = withTestingCell(rowsJson[index].cells.single_select_field.data);
|
||||||
|
|
||||||
|
singleSelectFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.SingleSelect));
|
||||||
|
cells.set('single_select_field', singleSelectFieldCell);
|
||||||
|
|
||||||
|
const multiSelectFieldCell = withTestingCell(rowsJson[index].cells.multi_select_field.data);
|
||||||
|
|
||||||
|
multiSelectFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.MultiSelect));
|
||||||
|
cells.set('multi_select_field', multiSelectFieldCell);
|
||||||
|
|
||||||
|
const checlistFieldCell = withTestingCell(rowsJson[index].cells.checklist_field.data);
|
||||||
|
|
||||||
|
checlistFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.Checklist));
|
||||||
|
cells.set('checklist_field', checlistFieldCell);
|
||||||
|
|
||||||
|
rowData.set(YjsDatabaseKey.cells, cells);
|
||||||
|
return rowData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTestingCell(cellData: string | number) {
|
||||||
|
const cell = new Y.Map() as YDatabaseCell;
|
||||||
|
|
||||||
|
cell.set(YjsDatabaseKey.data, cellData);
|
||||||
|
return cell;
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
import { YDatabaseSort, YjsDatabaseKey } from '@/application/collab.type';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import * as sortsJson from './fixtures/sorts.json';
|
||||||
|
|
||||||
|
export function withRichTextSort(isAscending: boolean = true) {
|
||||||
|
const sort = new Y.Map() as YDatabaseSort;
|
||||||
|
const sortJSON = isAscending ? sortsJson.sort_asc_text_field : sortsJson.sort_desc_text_field;
|
||||||
|
|
||||||
|
sort.set(YjsDatabaseKey.id, sortJSON.id);
|
||||||
|
sort.set(YjsDatabaseKey.field_id, sortJSON.field_id);
|
||||||
|
sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1');
|
||||||
|
|
||||||
|
return sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withUrlSort(isAscending: boolean = true) {
|
||||||
|
const sort = new Y.Map() as YDatabaseSort;
|
||||||
|
const sortJSON = isAscending ? sortsJson.sort_asc_url_field : sortsJson.sort_desc_url_field;
|
||||||
|
|
||||||
|
sort.set(YjsDatabaseKey.id, sortJSON.id);
|
||||||
|
sort.set(YjsDatabaseKey.field_id, sortJSON.field_id);
|
||||||
|
sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1');
|
||||||
|
|
||||||
|
return sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withNumberSort(isAscending: boolean = true) {
|
||||||
|
const sort = new Y.Map() as YDatabaseSort;
|
||||||
|
const sortJSON = isAscending ? sortsJson.sort_asc_number_field : sortsJson.sort_desc_number_field;
|
||||||
|
|
||||||
|
sort.set(YjsDatabaseKey.id, sortJSON.id);
|
||||||
|
sort.set(YjsDatabaseKey.field_id, sortJSON.field_id);
|
||||||
|
sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1');
|
||||||
|
|
||||||
|
return sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withCheckboxSort(isAscending: boolean = true) {
|
||||||
|
const sort = new Y.Map() as YDatabaseSort;
|
||||||
|
const sortJSON = isAscending ? sortsJson.sort_asc_checkbox_field : sortsJson.sort_desc_checkbox_field;
|
||||||
|
|
||||||
|
sort.set(YjsDatabaseKey.id, sortJSON.id);
|
||||||
|
sort.set(YjsDatabaseKey.field_id, sortJSON.field_id);
|
||||||
|
sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1');
|
||||||
|
|
||||||
|
return sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withDateTimeSort(isAscending: boolean = true) {
|
||||||
|
const sort = new Y.Map() as YDatabaseSort;
|
||||||
|
const sortJSON = isAscending ? sortsJson.sort_asc_date_field : sortsJson.sort_desc_date_field;
|
||||||
|
|
||||||
|
sort.set(YjsDatabaseKey.id, sortJSON.id);
|
||||||
|
sort.set(YjsDatabaseKey.field_id, sortJSON.field_id);
|
||||||
|
sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1');
|
||||||
|
|
||||||
|
return sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withSingleSelectOptionSort(isAscending: boolean = true) {
|
||||||
|
const sort = new Y.Map() as YDatabaseSort;
|
||||||
|
const sortJSON = isAscending ? sortsJson.sort_asc_single_select_field : sortsJson.sort_desc_single_select_field;
|
||||||
|
|
||||||
|
sort.set(YjsDatabaseKey.id, sortJSON.id);
|
||||||
|
sort.set(YjsDatabaseKey.field_id, sortJSON.field_id);
|
||||||
|
sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1');
|
||||||
|
|
||||||
|
return sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withMultiSelectOptionSort(isAscending: boolean = true) {
|
||||||
|
const sort = new Y.Map() as YDatabaseSort;
|
||||||
|
const sortJSON = isAscending ? sortsJson.sort_asc_multi_select_field : sortsJson.sort_desc_multi_select_field;
|
||||||
|
|
||||||
|
sort.set(YjsDatabaseKey.id, sortJSON.id);
|
||||||
|
sort.set(YjsDatabaseKey.field_id, sortJSON.field_id);
|
||||||
|
sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1');
|
||||||
|
|
||||||
|
return sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withChecklistSort(isAscending: boolean = true) {
|
||||||
|
const sort = new Y.Map() as YDatabaseSort;
|
||||||
|
const sortJSON = isAscending ? sortsJson.sort_asc_checklist_field : sortsJson.sort_desc_checklist_field;
|
||||||
|
|
||||||
|
sort.set(YjsDatabaseKey.id, sortJSON.id);
|
||||||
|
sort.set(YjsDatabaseKey.field_id, sortJSON.field_id);
|
||||||
|
sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1');
|
||||||
|
|
||||||
|
return sort;
|
||||||
|
}
|
@ -3,7 +3,7 @@ import { RowMetaKey } from '@/application/database-yjs/database.type';
|
|||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
import { v5 as uuidv5, parse as uuidParse } from 'uuid';
|
import { v5 as uuidv5, parse as uuidParse } from 'uuid';
|
||||||
|
|
||||||
export const DEFAULT_ROW_HEIGHT = 37;
|
export const DEFAULT_ROW_HEIGHT = 36;
|
||||||
export const MIN_COLUMN_WIDTH = 100;
|
export const MIN_COLUMN_WIDTH = 100;
|
||||||
|
|
||||||
export const getCell = (rowId: string, fieldId: string, rowMetas: Y.Map<YDoc>) => {
|
export const getCell = (rowId: string, fieldId: string, rowMetas: Y.Map<YDoc>) => {
|
||||||
|
@ -64,7 +64,7 @@ export const useDatabaseView = () => {
|
|||||||
const database = useDatabase();
|
const database = useDatabase();
|
||||||
const viewId = useViewId();
|
const viewId = useViewId();
|
||||||
|
|
||||||
return viewId ? database.get(YjsDatabaseKey.views)?.get(viewId) : undefined;
|
return viewId ? database?.get(YjsDatabaseKey.views)?.get(viewId) : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useDatabaseFields() {
|
export function useDatabaseFields() {
|
||||||
|
@ -141,6 +141,18 @@ export function textFilterCheck(data: string, content: string, condition: TextFi
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function numberFilterCheck(data: string, content: string, condition: number) {
|
export function numberFilterCheck(data: string, content: string, condition: number) {
|
||||||
|
if (isNaN(Number(data)) || isNaN(Number(content)) || data === '' || content === '') {
|
||||||
|
if (condition === NumberFilterCondition.NumberIsEmpty && data === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (condition === NumberFilterCondition.NumberIsNotEmpty && data !== '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const decimal = new Decimal(data).toNumber();
|
const decimal = new Decimal(data).toNumber();
|
||||||
const filterDecimal = new Decimal(content).toNumber();
|
const filterDecimal = new Decimal(content).toNumber();
|
||||||
|
|
||||||
@ -188,6 +200,14 @@ export function checklistFilterCheck(data: string, content: string, condition: n
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function selectOptionFilterCheck(data: string, content: string, condition: number) {
|
export function selectOptionFilterCheck(data: string, content: string, condition: number) {
|
||||||
|
if (SelectOptionFilterCondition.OptionIsEmpty === condition) {
|
||||||
|
return data === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SelectOptionFilterCondition.OptionIsNotEmpty === condition) {
|
||||||
|
return data !== '';
|
||||||
|
}
|
||||||
|
|
||||||
const selectedOptionIds = data.split(',');
|
const selectedOptionIds = data.split(',');
|
||||||
const filterOptionIds = content.split(',');
|
const filterOptionIds = content.split(',');
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ import { filterBy, parseFilter } from '@/application/database-yjs/filter';
|
|||||||
import { groupByField } from '@/application/database-yjs/group';
|
import { groupByField } from '@/application/database-yjs/group';
|
||||||
import { sortBy } from '@/application/database-yjs/sort';
|
import { sortBy } from '@/application/database-yjs/sort';
|
||||||
import { useViewsIdSelector } from '@/application/folder-yjs';
|
import { useViewsIdSelector } from '@/application/folder-yjs';
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
|
||||||
import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
|
import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
|
||||||
import { DateTimeCell } from '@/components/database/components/cell/cell.type';
|
import { DateTimeCell } from '@/components/database/components/cell/cell.type';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@ -44,10 +43,10 @@ export interface Row {
|
|||||||
|
|
||||||
const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty];
|
const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty];
|
||||||
|
|
||||||
export function useDatabaseViewsSelector() {
|
export function useDatabaseViewsSelector(iidIndex: string) {
|
||||||
const database = useDatabase();
|
const database = useDatabase();
|
||||||
const { objectId: currentViewId } = useId();
|
|
||||||
const { viewsId: visibleViewsId, views: folderViews } = useViewsIdSelector();
|
const { viewsId: visibleViewsId, views: folderViews } = useViewsIdSelector();
|
||||||
|
|
||||||
const views = database?.get(YjsDatabaseKey.views);
|
const views = database?.get(YjsDatabaseKey.views);
|
||||||
const [viewIds, setViewIds] = useState<string[]>([]);
|
const [viewIds, setViewIds] = useState<string[]>([]);
|
||||||
const childViews = useMemo(() => {
|
const childViews = useMemo(() => {
|
||||||
@ -58,16 +57,33 @@ export function useDatabaseViewsSelector() {
|
|||||||
if (!views) return;
|
if (!views) return;
|
||||||
|
|
||||||
const observerEvent = () => {
|
const observerEvent = () => {
|
||||||
setViewIds(
|
const viewsObj = views.toJSON();
|
||||||
Array.from(views.keys()).filter((id) => {
|
|
||||||
const view = folderViews?.get(id);
|
|
||||||
|
|
||||||
return (
|
const viewsSorted = Object.entries(viewsObj).sort((a, b) => {
|
||||||
visibleViewsId.includes(id) &&
|
const [, viewA] = a;
|
||||||
(view?.get(YjsFolderKey.bid) === currentViewId || view?.get(YjsFolderKey.id) === currentViewId)
|
const [, viewB] = b;
|
||||||
);
|
|
||||||
})
|
return Number(viewB.created_at) - Number(viewA.created_at);
|
||||||
);
|
});
|
||||||
|
|
||||||
|
const viewsId = [];
|
||||||
|
|
||||||
|
for (const viewItem of viewsSorted) {
|
||||||
|
const [key] = viewItem;
|
||||||
|
const view = folderViews?.get(key);
|
||||||
|
|
||||||
|
console.log('view', view?.get(YjsFolderKey.bid), iidIndex);
|
||||||
|
if (
|
||||||
|
visibleViewsId.includes(key) &&
|
||||||
|
view &&
|
||||||
|
(view.get(YjsFolderKey.bid) === iidIndex || view.get(YjsFolderKey.id) === iidIndex)
|
||||||
|
) {
|
||||||
|
viewsId.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('viewsId', viewsId);
|
||||||
|
setViewIds(viewsId);
|
||||||
};
|
};
|
||||||
|
|
||||||
observerEvent();
|
observerEvent();
|
||||||
@ -76,7 +92,7 @@ export function useDatabaseViewsSelector() {
|
|||||||
return () => {
|
return () => {
|
||||||
views.unobserve(observerEvent);
|
views.unobserve(observerEvent);
|
||||||
};
|
};
|
||||||
}, [visibleViewsId, views, folderViews, currentViewId]);
|
}, [visibleViewsId, views, folderViews, iidIndex]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
childViews,
|
childViews,
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
import { FieldType, SortCondition } from '@/application/database-yjs/database.type';
|
import { FieldType, SortCondition } from '@/application/database-yjs/database.type';
|
||||||
import { parseChecklistData, parseSelectOptionCellData } from '@/application/database-yjs/fields';
|
import { parseChecklistData, parseSelectOptionCellData } from '@/application/database-yjs/fields';
|
||||||
import { Row } from '@/application/database-yjs/selector';
|
import { Row } from '@/application/database-yjs/selector';
|
||||||
import orderBy from 'lodash-es/orderBy';
|
import { orderBy } from 'lodash-es';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
export function sortBy(rows: Row[], sorts: YDatabaseSorts, fields: YDatabaseFields, rowMetas: Y.Map<YDoc>) {
|
export function sortBy(rows: Row[], sorts: YDatabaseSorts, fields: YDatabaseFields, rowMetas: Y.Map<YDoc>) {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
export enum CoverType {
|
export enum CoverType {
|
||||||
NormalColor = 'color',
|
NormalColor = 'color',
|
||||||
GradientColor = 'gradient',
|
GradientColor = 'gradient',
|
||||||
BuildInImage = 'none',
|
BuildInImage = 'built_in',
|
||||||
CustomImage = 'custom',
|
CustomImage = 'custom',
|
||||||
LocalImage = 'local',
|
LocalImage = 'local',
|
||||||
UpsplashImage = 'unsplash',
|
UpsplashImage = 'unsplash',
|
||||||
|
None = 'none',
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,9 @@ export function useViewsIdSelector() {
|
|||||||
const meta = folder?.get(YjsFolderKey.meta);
|
const meta = folder?.get(YjsFolderKey.meta);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!views) return;
|
if (!views) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const trashUid = trash ? Array.from(trash.keys())[0] : null;
|
const trashUid = trash ? Array.from(trash.keys())[0] : null;
|
||||||
const userTrash = trashUid ? trash?.get(trashUid) : null;
|
const userTrash = trashUid ? trash?.get(trashUid) : null;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { CollabOrigin, CollabType, YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
import { CollabType, YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||||
import {
|
import {
|
||||||
batchCollabs,
|
batchCollabs,
|
||||||
getCollabStorage,
|
getCollabStorage,
|
||||||
getCollabStorageWithAPICall,
|
getCollabStorageWithAPICall,
|
||||||
getUserWorkspace,
|
getCurrentWorkspace,
|
||||||
} from '@/application/services/js-services/storage';
|
} from '@/application/services/js-services/storage';
|
||||||
import { DatabaseService } from '@/application/services/services.type';
|
import { DatabaseService } from '@/application/services/services.type';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
@ -17,14 +17,43 @@ export class JSDatabaseService implements DatabaseService {
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDatabase(
|
currentWorkspace() {
|
||||||
workspaceId: string,
|
return getCurrentWorkspace();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkspaceDatabases(): Promise<{ views: string[]; database_id: string }[]> {
|
||||||
|
const workspace = await this.currentWorkspace();
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error('Workspace database not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceDatabase = await getCollabStorageWithAPICall(
|
||||||
|
workspace.id,
|
||||||
|
workspace.workspaceDatabaseId,
|
||||||
|
CollabType.WorkspaceDatabase
|
||||||
|
);
|
||||||
|
|
||||||
|
return workspaceDatabase.getMap(YjsEditorKey.data_section).get(YjsEditorKey.workspace_database).toJSON() as {
|
||||||
|
views: string[];
|
||||||
|
database_id: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async openDatabase(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
rowIds?: string[]
|
rowIds?: string[]
|
||||||
): Promise<{
|
): Promise<{
|
||||||
databaseDoc: YDoc;
|
databaseDoc: YDoc;
|
||||||
rows: Y.Map<YDoc>;
|
rows: Y.Map<YDoc>;
|
||||||
}> {
|
}> {
|
||||||
|
const workspace = await this.currentWorkspace();
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error('Workspace database not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceId = workspace.id;
|
||||||
const isLoaded = this.loadedDatabaseId.has(databaseId);
|
const isLoaded = this.loadedDatabaseId.has(databaseId);
|
||||||
|
|
||||||
const rootRowsDoc =
|
const rootRowsDoc =
|
||||||
@ -106,68 +135,6 @@ export class JSDatabaseService implements DatabaseService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async openDatabase(
|
|
||||||
workspaceId: string,
|
|
||||||
viewId: string,
|
|
||||||
rowIds?: string[]
|
|
||||||
): Promise<{
|
|
||||||
databaseDoc: YDoc;
|
|
||||||
rows: Y.Map<YDoc>;
|
|
||||||
}> {
|
|
||||||
const userWorkspace = await getUserWorkspace();
|
|
||||||
|
|
||||||
if (!userWorkspace) {
|
|
||||||
throw new Error('User workspace not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceDatabaseId = userWorkspace.workspaces.find(
|
|
||||||
(workspace) => workspace.id === workspaceId
|
|
||||||
)?.workspaceDatabaseId;
|
|
||||||
|
|
||||||
if (!workspaceDatabaseId) {
|
|
||||||
throw new Error('Workspace database not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceDatabase = await getCollabStorageWithAPICall(
|
|
||||||
workspaceId,
|
|
||||||
workspaceDatabaseId,
|
|
||||||
CollabType.WorkspaceDatabase
|
|
||||||
);
|
|
||||||
|
|
||||||
const databases = workspaceDatabase
|
|
||||||
.getMap(YjsEditorKey.data_section)
|
|
||||||
.get(YjsEditorKey.workspace_database)
|
|
||||||
.toJSON() as {
|
|
||||||
views: string[];
|
|
||||||
database_id: string;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
const databaseMeta = databases.find((item) => {
|
|
||||||
return item.views.some((databaseViewId: string) => databaseViewId === viewId);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!databaseMeta) {
|
|
||||||
throw new Error('Database not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { databaseDoc, rows } = await this.getDatabase(workspaceId, databaseMeta.database_id, rowIds);
|
|
||||||
|
|
||||||
const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => {
|
|
||||||
if (origin === CollabOrigin.LocalSync) {
|
|
||||||
// Send the update to the server
|
|
||||||
console.log('update', update);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
databaseDoc.on('update', handleUpdate);
|
|
||||||
console.log('Database loaded', rows.toJSON());
|
|
||||||
|
|
||||||
return {
|
|
||||||
databaseDoc,
|
|
||||||
rows,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadDatabaseRows(workspaceId: string, rowIds: string[], rowCallback: (rowId: string, rowDoc: YDoc) => void) {
|
async loadDatabaseRows(workspaceId: string, rowIds: string[], rowCallback: (rowId: string, rowDoc: YDoc) => void) {
|
||||||
try {
|
try {
|
||||||
await batchCollabs(
|
await batchCollabs(
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type';
|
import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type';
|
||||||
import { getCollabStorageWithAPICall } from '@/application/services/js-services/storage';
|
import { getCollabStorageWithAPICall, getCurrentWorkspace } from '@/application/services/js-services/storage';
|
||||||
import { DocumentService } from '@/application/services/services.type';
|
import { DocumentService } from '@/application/services/services.type';
|
||||||
|
|
||||||
export class JSDocumentService implements DocumentService {
|
export class JSDocumentService implements DocumentService {
|
||||||
@ -7,8 +7,14 @@ export class JSDocumentService implements DocumentService {
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
async openDocument(workspaceId: string, docId: string): Promise<YDoc> {
|
async openDocument(docId: string): Promise<YDoc> {
|
||||||
const doc = await getCollabStorageWithAPICall(workspaceId, docId, CollabType.Document);
|
const workspace = await getCurrentWorkspace();
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error('Workspace database not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = await getCollabStorageWithAPICall(workspace.id, docId, CollabType.Document);
|
||||||
|
|
||||||
const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => {
|
const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => {
|
||||||
if (origin === CollabOrigin.LocalSync) {
|
if (origin === CollabOrigin.LocalSync) {
|
||||||
|
@ -110,7 +110,7 @@ export async function batchCollabs(
|
|||||||
|
|
||||||
const { doc } = await getCollabStorage(id, type);
|
const { doc } = await getCollabStorage(id, type);
|
||||||
|
|
||||||
applyYDoc(doc, data);
|
applyYDoc(doc, new Uint8Array(data));
|
||||||
|
|
||||||
rowCallback?.(id, doc);
|
rowCallback?.(id, doc);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { UserProfile, UserWorkspace } from '@/application/user.type';
|
import { UserProfile, UserWorkspace, Workspace } from '@/application/user.type';
|
||||||
|
|
||||||
const userKey = 'user';
|
const userKey = 'user';
|
||||||
const workspaceKey = 'workspace';
|
const workspaceKey = 'workspace';
|
||||||
@ -34,3 +34,10 @@ export async function setUserWorkspace(workspace: UserWorkspace) {
|
|||||||
|
|
||||||
localStorage.setItem(workspaceKey, str);
|
localStorage.setItem(workspaceKey, str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCurrentWorkspace(): Promise<Workspace | undefined> {
|
||||||
|
const userProfile = await getSignInUser();
|
||||||
|
const userWorkspace = await getUserWorkspace();
|
||||||
|
|
||||||
|
return userWorkspace?.workspaces.find((workspace) => workspace.id === userProfile?.workspaceId);
|
||||||
|
}
|
||||||
|
@ -93,10 +93,10 @@ export async function batchGetCollab(
|
|||||||
}))
|
}))
|
||||||
)) as unknown as Map<string, { doc_state: number[] }>;
|
)) as unknown as Map<string, { doc_state: number[] }>;
|
||||||
|
|
||||||
const result: Record<string, Uint8Array> = {};
|
const result: Record<string, number[]> = {};
|
||||||
|
|
||||||
res.forEach((value, key) => {
|
res.forEach((value, key) => {
|
||||||
result[key] = new Uint8Array(value.doc_state);
|
result[key] = value.doc_state;
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -31,20 +31,12 @@ export interface AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentService {
|
export interface DocumentService {
|
||||||
openDocument: (workspaceId: string, docId: string) => Promise<YDoc>;
|
openDocument: (docId: string) => Promise<YDoc>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseService {
|
export interface DatabaseService {
|
||||||
|
getWorkspaceDatabases: () => Promise<{ views: string[]; database_id: string }[]>;
|
||||||
openDatabase: (
|
openDatabase: (
|
||||||
workspaceId: string,
|
|
||||||
viewId: string,
|
|
||||||
rowIds?: string[]
|
|
||||||
) => Promise<{
|
|
||||||
databaseDoc: YDoc;
|
|
||||||
rows: Y.Map<YDoc>;
|
|
||||||
}>;
|
|
||||||
getDatabase: (
|
|
||||||
workspaceId: string,
|
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
rowIds?: string[]
|
rowIds?: string[]
|
||||||
) => Promise<{
|
) => Promise<{
|
||||||
|
@ -7,24 +7,15 @@ export class TauriDatabaseService implements DatabaseService {
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getWorkspaceDatabases(): Promise<{ views: string[]; database_id: string }[]> {
|
||||||
|
return Promise.reject('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
async closeDatabase(_databaseId: string) {
|
async closeDatabase(_databaseId: string) {
|
||||||
return Promise.reject('Not implemented');
|
return Promise.reject('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
async openDatabase(
|
async openDatabase(_viewId: string): Promise<{
|
||||||
_workspaceId: string,
|
|
||||||
_viewId: string
|
|
||||||
): Promise<{
|
|
||||||
databaseDoc: YDoc;
|
|
||||||
rows: Y.Map<YDoc>;
|
|
||||||
}> {
|
|
||||||
return Promise.reject('Not implemented');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDatabase(
|
|
||||||
_workspaceId: string,
|
|
||||||
_databaseId: string
|
|
||||||
): Promise<{
|
|
||||||
databaseDoc: YDoc;
|
databaseDoc: YDoc;
|
||||||
rows: Y.Map<YDoc>;
|
rows: Y.Map<YDoc>;
|
||||||
}> {
|
}> {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { CollabOrigin, YjsEditorKey, YSharedRoot } from '@/application/collab.type';
|
import { CollabOrigin, YjsEditorKey, YSharedRoot } from '@/application/collab.type';
|
||||||
import { applySlateOp } from '@/application/slate-yjs/utils/applySlateOpts';
|
import { applyToYjs } from '@/application/slate-yjs/utils/applyToYjs';
|
||||||
import { translateYjsEvent } from 'src/application/slate-yjs/utils/translateYjsEvent';
|
|
||||||
import { Editor, Operation, Descendant } from 'slate';
|
import { Editor, Operation, Descendant } from 'slate';
|
||||||
import Y, { YEvent, Transaction } from 'yjs';
|
import Y, { YEvent, Transaction } from 'yjs';
|
||||||
import { yDocToSlateContent } from '@/application/slate-yjs/utils/convert';
|
import { yDocToSlateContent } from '@/application/slate-yjs/utils/convert';
|
||||||
@ -57,12 +56,11 @@ export const YjsEditor = {
|
|||||||
export function withYjs<T extends Editor>(
|
export function withYjs<T extends Editor>(
|
||||||
editor: T,
|
editor: T,
|
||||||
doc: Y.Doc,
|
doc: Y.Doc,
|
||||||
{
|
opts?: {
|
||||||
localOrigin,
|
|
||||||
}: {
|
|
||||||
localOrigin: CollabOrigin;
|
localOrigin: CollabOrigin;
|
||||||
}
|
}
|
||||||
): T & YjsEditor {
|
): T & YjsEditor {
|
||||||
|
const { localOrigin = CollabOrigin.Local } = opts ?? {};
|
||||||
const e = editor as T & YjsEditor;
|
const e = editor as T & YjsEditor;
|
||||||
const { apply, onChange } = e;
|
const { apply, onChange } = e;
|
||||||
|
|
||||||
@ -76,23 +74,34 @@ export function withYjs<T extends Editor>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.children = content.children;
|
e.children = content.children;
|
||||||
|
|
||||||
|
console.log('initializeDocumentContent', doc.getMap(YjsEditorKey.data_section).toJSON(), e.children);
|
||||||
Editor.normalize(editor, { force: true });
|
Editor.normalize(editor, { force: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
e.applyRemoteEvents = (events: Array<YEvent<YSharedRoot>>, _: Transaction) => {
|
const applyIntercept = (op: Operation) => {
|
||||||
YjsEditor.flushLocalChanges(e);
|
if (YjsEditor.connected(e)) {
|
||||||
|
YjsEditor.storeLocalChange(e, op);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: handle remote events
|
apply(op);
|
||||||
// This is a temporary implementation to apply remote events to slate
|
};
|
||||||
|
|
||||||
|
const applyRemoteIntercept = (op: Operation) => {
|
||||||
|
apply(op);
|
||||||
|
};
|
||||||
|
|
||||||
|
e.applyRemoteEvents = (_events: Array<YEvent<YSharedRoot>>, _: 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
|
||||||
|
e.apply = applyRemoteIntercept;
|
||||||
|
|
||||||
|
// Initialize or update the document content to ensure it is in the correct state before applying remote events
|
||||||
initializeDocumentContent();
|
initializeDocumentContent();
|
||||||
Editor.withoutNormalizing(editor, () => {
|
|
||||||
events.forEach((event) => {
|
// Restore the apply function to store local changes after applying remote changes
|
||||||
translateYjsEvent(e.sharedRoot, editor, event).forEach((op) => {
|
e.apply = applyIntercept;
|
||||||
// apply remote events to slate, don't call e.apply here because e.apply has been overridden.
|
|
||||||
apply(op);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleYEvents = (events: Array<YEvent<YSharedRoot>>, transaction: Transaction) => {
|
const handleYEvents = (events: Array<YEvent<YSharedRoot>>, transaction: Transaction) => {
|
||||||
@ -133,18 +142,12 @@ export function withYjs<T extends Editor>(
|
|||||||
// parse changes and apply to ydoc
|
// parse changes and apply to ydoc
|
||||||
doc.transact(() => {
|
doc.transact(() => {
|
||||||
changes.forEach((change) => {
|
changes.forEach((change) => {
|
||||||
applySlateOp(doc, { children: change.slateContent }, change.op);
|
applyToYjs(doc, { children: change.slateContent }, change.op);
|
||||||
});
|
});
|
||||||
}, localOrigin);
|
}, localOrigin);
|
||||||
};
|
};
|
||||||
|
|
||||||
e.apply = (op) => {
|
e.apply = applyIntercept;
|
||||||
if (YjsEditor.connected(e)) {
|
|
||||||
YjsEditor.storeLocalChange(e, op);
|
|
||||||
}
|
|
||||||
|
|
||||||
apply(op);
|
|
||||||
};
|
|
||||||
|
|
||||||
e.onChange = () => {
|
e.onChange = () => {
|
||||||
if (YjsEditor.connected(e)) {
|
if (YjsEditor.connected(e)) {
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
CollabOrigin,
|
||||||
|
YBlocks,
|
||||||
|
YChildrenMap,
|
||||||
|
YjsEditorKey,
|
||||||
|
YMeta,
|
||||||
|
YSharedRoot,
|
||||||
|
YTextMap,
|
||||||
|
} from '@/application/collab.type';
|
||||||
|
import { yDocToSlateContent } from '@/application/slate-yjs/utils/convert';
|
||||||
|
import { generateId, withTestingYDoc, withTestingYjsEditor } from './withTestingYjsEditor';
|
||||||
|
import { createEditor } from 'slate';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
|
export async function runApplyRemoteEventsTest() {
|
||||||
|
const pageId = generateId();
|
||||||
|
const remoteDoc = withTestingYDoc(pageId);
|
||||||
|
const remote = withTestingYjsEditor(createEditor(), remoteDoc);
|
||||||
|
|
||||||
|
const localDoc = new Y.Doc();
|
||||||
|
|
||||||
|
Y.applyUpdateV2(localDoc, Y.encodeStateAsUpdateV2(remoteDoc));
|
||||||
|
const editor = withTestingYjsEditor(createEditor(), localDoc);
|
||||||
|
|
||||||
|
editor.connect();
|
||||||
|
expect(editor.children).toEqual(remote.children);
|
||||||
|
|
||||||
|
// update remote doc
|
||||||
|
insertBlock(remoteDoc, generateId(), pageId, 0);
|
||||||
|
remote.children = yDocToSlateContent(remoteDoc)?.children ?? [];
|
||||||
|
|
||||||
|
// apply remote changes to local doc
|
||||||
|
Y.transact(
|
||||||
|
localDoc,
|
||||||
|
() => {
|
||||||
|
Y.applyUpdateV2(localDoc, Y.encodeStateAsUpdateV2(remoteDoc));
|
||||||
|
},
|
||||||
|
CollabOrigin.Remote
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(editor.children).toEqual(remote.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertBlock(doc: Y.Doc, blockId: string, parentId: string, index: number) {
|
||||||
|
const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
||||||
|
const document = sharedRoot.get(YjsEditorKey.document);
|
||||||
|
const blocks = document.get(YjsEditorKey.blocks) as YBlocks;
|
||||||
|
const meta = document.get(YjsEditorKey.meta) as YMeta;
|
||||||
|
const childrenMap = meta.get(YjsEditorKey.children_map) as YChildrenMap;
|
||||||
|
const textMap = meta.get(YjsEditorKey.text_map) as YTextMap;
|
||||||
|
|
||||||
|
const block = new Y.Map();
|
||||||
|
|
||||||
|
block.set(YjsEditorKey.block_id, blockId);
|
||||||
|
block.set(YjsEditorKey.block_children, blockId);
|
||||||
|
block.set(YjsEditorKey.block_type, 'paragraph');
|
||||||
|
block.set(YjsEditorKey.block_data, '{}');
|
||||||
|
block.set(YjsEditorKey.block_external_id, blockId);
|
||||||
|
blocks.set(blockId, block);
|
||||||
|
childrenMap.set(blockId, new Y.Array());
|
||||||
|
childrenMap.get(parentId).insert(index, [blockId]);
|
||||||
|
const text = new Y.Text();
|
||||||
|
|
||||||
|
text.insert(0, 'Hello, World!');
|
||||||
|
textMap.set(blockId, text);
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
import { withTestingYDoc, withTestingYjsEditor } from './withTestingYjsEditor';
|
||||||
|
import { yDocToSlateContent } from '../convert';
|
||||||
|
import { createEditor, Editor } from 'slate';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
|
function normalizedSlateDoc(doc: Y.Doc) {
|
||||||
|
const editor = createEditor();
|
||||||
|
|
||||||
|
const yjsEditor = withTestingYjsEditor(editor, doc);
|
||||||
|
|
||||||
|
editor.children = yDocToSlateContent(doc)?.children ?? [];
|
||||||
|
return yjsEditor.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCollaborationTest() {
|
||||||
|
const doc = withTestingYDoc('1');
|
||||||
|
const editor = createEditor();
|
||||||
|
const yjsEditor = withTestingYjsEditor(editor, doc);
|
||||||
|
|
||||||
|
// Keep the 'local' editor state before applying run.
|
||||||
|
const baseState = Y.encodeStateAsUpdateV2(doc);
|
||||||
|
|
||||||
|
Editor.normalize(editor, { force: true });
|
||||||
|
|
||||||
|
expect(normalizedSlateDoc(doc)).toEqual(yjsEditor.children);
|
||||||
|
|
||||||
|
// Setup remote editor with input base state
|
||||||
|
const remoteDoc = new Y.Doc();
|
||||||
|
|
||||||
|
Y.applyUpdateV2(remoteDoc, baseState);
|
||||||
|
const remote = withTestingYjsEditor(createEditor(), remoteDoc);
|
||||||
|
|
||||||
|
// Apply changes from 'run'
|
||||||
|
Y.applyUpdateV2(remoteDoc, Y.encodeStateAsUpdateV2(yjsEditor.sharedRoot.doc!));
|
||||||
|
|
||||||
|
// Verify remote and editor state are equal
|
||||||
|
expect(normalizedSlateDoc(remoteDoc)).toEqual(remote.children);
|
||||||
|
expect(yjsEditor.children).toEqual(remote.children);
|
||||||
|
expect(normalizedSlateDoc(doc)).toEqual(yjsEditor.children);
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import { runCollaborationTest } from './convert';
|
||||||
|
import { runApplyRemoteEventsTest } from './applyRemoteEvents';
|
||||||
|
|
||||||
|
describe('slate-yjs adapter', () => {
|
||||||
|
it('should pass the collaboration test', async () => {
|
||||||
|
await runCollaborationTest();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass the apply remote events test', async () => {
|
||||||
|
await runApplyRemoteEventsTest();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,45 @@
|
|||||||
|
import { CollabOrigin, YjsEditorKey, YSharedRoot } from '@/application/collab.type';
|
||||||
|
import { withYjs } from '@/application/slate-yjs';
|
||||||
|
import { Editor } from 'slate';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
export function generateId() {
|
||||||
|
return uuidv4();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTestingYjsEditor(editor: Editor, doc: Y.Doc) {
|
||||||
|
const yjdEditor = withYjs(editor, doc, {
|
||||||
|
localOrigin: CollabOrigin.LocalSync,
|
||||||
|
});
|
||||||
|
|
||||||
|
return yjdEditor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTestingYDoc(docId: string) {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
||||||
|
const document = new Y.Map();
|
||||||
|
const blocks = new Y.Map();
|
||||||
|
const meta = new Y.Map();
|
||||||
|
const children_map = new Y.Map();
|
||||||
|
const text_map = new Y.Map();
|
||||||
|
const rootBlock = new Y.Map();
|
||||||
|
const blockOrders = new Y.Array();
|
||||||
|
const pageId = docId;
|
||||||
|
|
||||||
|
sharedRoot.set(YjsEditorKey.document, document);
|
||||||
|
document.set(YjsEditorKey.page_id, pageId);
|
||||||
|
document.set(YjsEditorKey.blocks, blocks);
|
||||||
|
document.set(YjsEditorKey.meta, meta);
|
||||||
|
meta.set(YjsEditorKey.children_map, children_map);
|
||||||
|
meta.set(YjsEditorKey.text_map, text_map);
|
||||||
|
children_map.set(pageId, blockOrders);
|
||||||
|
blocks.set(pageId, rootBlock);
|
||||||
|
rootBlock.set(YjsEditorKey.block_id, pageId);
|
||||||
|
rootBlock.set(YjsEditorKey.block_children, pageId);
|
||||||
|
rootBlock.set(YjsEditorKey.block_type, 'page');
|
||||||
|
rootBlock.set(YjsEditorKey.block_data, '{}');
|
||||||
|
rootBlock.set(YjsEditorKey.block_external_id, '');
|
||||||
|
return doc;
|
||||||
|
}
|
@ -1,7 +0,0 @@
|
|||||||
import { Operation, Node } from 'slate';
|
|
||||||
import * as Y from 'yjs';
|
|
||||||
|
|
||||||
// transform slate op to yjs op and apply it to ydoc
|
|
||||||
export function applySlateOp(_ydoc: Y.Doc, _slateRoot: Node, _op: Operation) {
|
|
||||||
// console.log('applySlateOp', op);
|
|
||||||
}
|
|
@ -0,0 +1,8 @@
|
|||||||
|
import { Operation, Node } from 'slate';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
|
// transform slate op to yjs op and apply it to ydoc
|
||||||
|
export function applyToYjs(_ydoc: Y.Doc, _slateRoot: Node, op: Operation) {
|
||||||
|
if (op.type === 'set_selection') return;
|
||||||
|
console.log('applySlateOp', op);
|
||||||
|
}
|
@ -10,22 +10,14 @@ import {
|
|||||||
BlockData,
|
BlockData,
|
||||||
BlockType,
|
BlockType,
|
||||||
} from '@/application/collab.type';
|
} from '@/application/collab.type';
|
||||||
|
import { BlockJson } from '@/application/slate-yjs/utils/types';
|
||||||
import { getFontFamily } from '@/utils/font';
|
import { getFontFamily } from '@/utils/font';
|
||||||
import { uniq } from 'lodash-es';
|
import { uniq } from 'lodash-es';
|
||||||
import { Element, Text } from 'slate';
|
import { Element, Text } from 'slate';
|
||||||
|
|
||||||
interface BlockJson {
|
|
||||||
id: string;
|
|
||||||
ty: string;
|
|
||||||
data?: string;
|
|
||||||
children?: string;
|
|
||||||
external_id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function yDocToSlateContent(doc: YDoc): Element | undefined {
|
export function yDocToSlateContent(doc: YDoc): Element | undefined {
|
||||||
const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
||||||
|
|
||||||
console.log(sharedRoot.toJSON());
|
|
||||||
const document = sharedRoot.get(YjsEditorKey.document);
|
const document = sharedRoot.get(YjsEditorKey.document);
|
||||||
const pageId = document.get(YjsEditorKey.page_id) as string;
|
const pageId = document.get(YjsEditorKey.page_id) as string;
|
||||||
const blocks = document.get(YjsEditorKey.blocks) as YBlocks;
|
const blocks = document.get(YjsEditorKey.blocks) as YBlocks;
|
||||||
@ -129,6 +121,7 @@ export function blockToSlateNode(block: BlockJson): Element {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
blockId: block.id,
|
blockId: block.id,
|
||||||
|
relationId: block.children,
|
||||||
data: blockData,
|
data: blockData,
|
||||||
type: block.ty,
|
type: block.ty,
|
||||||
children: [],
|
children: [],
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import { YSharedRoot } from '@/application/collab.type';
|
|
||||||
import * as Y from 'yjs';
|
|
||||||
import { Editor, Operation } from 'slate';
|
|
||||||
|
|
||||||
export function translateYArrayEvent(
|
|
||||||
_sharedRoot: YSharedRoot,
|
|
||||||
_editor: Editor,
|
|
||||||
_event: Y.YEvent<Y.Array<string>>
|
|
||||||
): Operation[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import { YSharedRoot } from '@/application/collab.type';
|
|
||||||
import { translateYArrayEvent } from '@/application/slate-yjs/utils/translateYjsEvent/arrayEvent';
|
|
||||||
import { translateYMapEvent } from '@/application/slate-yjs/utils/translateYjsEvent/mapEvent';
|
|
||||||
import { Editor, Operation } from 'slate';
|
|
||||||
import * as Y from 'yjs';
|
|
||||||
import { translateYTextEvent } from 'src/application/slate-yjs/utils/translateYjsEvent/textEvent';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translate a yjs event into slate operations. The editor state has to match the
|
|
||||||
* yText state before the event occurred.
|
|
||||||
*
|
|
||||||
* @param sharedType
|
|
||||||
* @param op
|
|
||||||
*/
|
|
||||||
export function translateYjsEvent(sharedRoot: YSharedRoot, editor: Editor, event: Y.YEvent<YSharedRoot>): Operation[] {
|
|
||||||
if (event instanceof Y.YMapEvent) {
|
|
||||||
return translateYMapEvent(sharedRoot, editor, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event instanceof Y.YTextEvent) {
|
|
||||||
return translateYTextEvent(sharedRoot, editor, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event instanceof Y.YArrayEvent) {
|
|
||||||
return translateYArrayEvent(sharedRoot, editor, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Unexpected Y event type');
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import { YSharedRoot } from '@/application/collab.type';
|
|
||||||
import * as Y from 'yjs';
|
|
||||||
import { Editor, Operation } from 'slate';
|
|
||||||
|
|
||||||
export function translateYMapEvent(
|
|
||||||
_sharedRoot: YSharedRoot,
|
|
||||||
_editor: Editor,
|
|
||||||
_event: Y.YEvent<Y.Map<unknown>>
|
|
||||||
): Operation[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import { YSharedRoot } from '@/application/collab.type';
|
|
||||||
import * as Y from 'yjs';
|
|
||||||
import { Editor, Operation } from 'slate';
|
|
||||||
|
|
||||||
export function translateYTextEvent(_sharedRoot: YSharedRoot, _editor: Editor, _event: Y.YEvent<Y.Text>): Operation[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Node as SlateNode } from 'slate';
|
||||||
|
|
||||||
|
export interface BlockJson {
|
||||||
|
id: string;
|
||||||
|
ty: string;
|
||||||
|
data?: string;
|
||||||
|
children?: string;
|
||||||
|
external_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Operation {
|
||||||
|
type: OperationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OperationType {
|
||||||
|
InsertNode = 'insert_node',
|
||||||
|
InsertChildren = 'insert_children',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InsertNodeOperation extends Operation {
|
||||||
|
type: OperationType.InsertNode;
|
||||||
|
node: SlateNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InsertChildrenOperation extends Operation {
|
||||||
|
type: OperationType.InsertChildren;
|
||||||
|
blockId: string;
|
||||||
|
children: string[];
|
||||||
|
}
|
BIN
frontend/appflowy_web_app/src/assets/cover/m_cover_image_1.png
Normal file
After Width: | Height: | Size: 2.2 MiB |
BIN
frontend/appflowy_web_app/src/assets/cover/m_cover_image_2.png
Normal file
After Width: | Height: | Size: 731 KiB |
BIN
frontend/appflowy_web_app/src/assets/cover/m_cover_image_3.png
Normal file
After Width: | Height: | Size: 465 KiB |
BIN
frontend/appflowy_web_app/src/assets/cover/m_cover_image_4.png
Normal file
After Width: | Height: | Size: 526 KiB |
BIN
frontend/appflowy_web_app/src/assets/cover/m_cover_image_5.png
Normal file
After Width: | Height: | Size: 293 KiB |
BIN
frontend/appflowy_web_app/src/assets/cover/m_cover_image_6.png
Normal file
After Width: | Height: | Size: 765 KiB |
@ -3,7 +3,6 @@ import { useContext, createContext } from 'react';
|
|||||||
export const IdContext = createContext<IdProviderProps | null>(null);
|
export const IdContext = createContext<IdProviderProps | null>(null);
|
||||||
|
|
||||||
interface IdProviderProps {
|
interface IdProviderProps {
|
||||||
workspaceId: string;
|
|
||||||
objectId: string;
|
objectId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import { getCurrentWorkspace } from '@/application/services/js-services/storage';
|
||||||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material';
|
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export function RecordNotFound({ open, workspaceId, title }: { workspaceId: string; open: boolean; title?: string }) {
|
export function RecordNotFound({ open, title }: { open: boolean; title?: string }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -15,8 +16,11 @@ export function RecordNotFound({ open, workspaceId, title }: { workspaceId: stri
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions className={'flex w-full items-center justify-center'}>
|
<DialogActions className={'flex w-full items-center justify-center'}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
navigate(`/view/${workspaceId}`);
|
const workspace = await getCurrentWorkspace();
|
||||||
|
|
||||||
|
if (!workspace) return;
|
||||||
|
navigate(`/view/${workspace.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Go back
|
Go back
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
import { YDoc, YFolder, YjsEditorKey } from '@/application/collab.type';
|
||||||
|
import { applyYDoc } from '@/application/ydoc/apply';
|
||||||
|
import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider';
|
||||||
|
import { IdProvider } from '@/components/_shared/context-provider/IdProvider';
|
||||||
|
import withAppWrapper from '@/components/app/withAppWrapper';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Database } from './Database';
|
||||||
|
import { DatabaseContextProvider } from './DatabaseContext';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import '@/components/layout/layout.scss';
|
||||||
|
|
||||||
|
describe('<Database />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.viewport(1280, 720);
|
||||||
|
Object.defineProperty(window.navigator, 'language', { value: 'en-US' });
|
||||||
|
Object.defineProperty(window.navigator, 'languages', { value: ['en-US'] });
|
||||||
|
cy.mockDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with a database', () => {
|
||||||
|
cy.fixture('folder').then((folderJson) => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const state = new Uint8Array(folderJson.data.doc_state);
|
||||||
|
|
||||||
|
applyYDoc(doc, state);
|
||||||
|
|
||||||
|
const folder = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.folder) as YFolder;
|
||||||
|
|
||||||
|
cy.fixture(`database/4c658817-20db-4f56-b7f9-0637a22dfeb6`).then((database) => {
|
||||||
|
cy.fixture(`database/rows/4c658817-20db-4f56-b7f9-0637a22dfeb6`).then((rows) => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const rootRowsDoc = new Y.Doc();
|
||||||
|
const rowsFolder: Y.Map<YDoc> = rootRowsDoc.getMap();
|
||||||
|
const databaseState = new Uint8Array(database.data.doc_state);
|
||||||
|
|
||||||
|
applyYDoc(doc, databaseState);
|
||||||
|
|
||||||
|
Object.keys(rows).forEach((key) => {
|
||||||
|
const data = rows[key];
|
||||||
|
const rowDoc = new Y.Doc();
|
||||||
|
|
||||||
|
applyYDoc(rowDoc, new Uint8Array(data));
|
||||||
|
rowsFolder.set(key, rowDoc);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onNavigateToView = cy.stub();
|
||||||
|
|
||||||
|
const AppWrapper = withAppWrapper(() => {
|
||||||
|
return (
|
||||||
|
<div className={'flex h-screen w-screen flex-col py-4'}>
|
||||||
|
<TestDatabase
|
||||||
|
databaseDoc={doc}
|
||||||
|
rows={rowsFolder}
|
||||||
|
folder={folder}
|
||||||
|
iidIndex={'7d2148fc-cace-4452-9c5c-96e52e6bf8b5'}
|
||||||
|
initialViewId={'7d2148fc-cace-4452-9c5c-96e52e6bf8b5'}
|
||||||
|
onNavigateToView={onNavigateToView}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.mount(<AppWrapper />);
|
||||||
|
|
||||||
|
cy.get('[data-testid^=view-tab-]').should('have.length', 4);
|
||||||
|
cy.get('.database-grid').should('exist');
|
||||||
|
|
||||||
|
cy.get('[data-testid=view-tab-e410747b-5f2f-45a0-b2f7-890ad3001355]').click();
|
||||||
|
cy.get('.database-board').should('exist');
|
||||||
|
cy.wrap(onNavigateToView).should('have.been.calledOnceWith', 'e410747b-5f2f-45a0-b2f7-890ad3001355');
|
||||||
|
|
||||||
|
cy.wait(800);
|
||||||
|
cy.get('[data-testid=view-tab-7d2148fc-cace-4452-9c5c-96e52e6bf8b5]').click();
|
||||||
|
cy.get('.database-grid').should('exist');
|
||||||
|
cy.wrap(onNavigateToView).should('have.been.calledWith', '7d2148fc-cace-4452-9c5c-96e52e6bf8b5');
|
||||||
|
|
||||||
|
cy.wait(800);
|
||||||
|
cy.get('[data-testid=view-tab-2143e95d-5dcb-4e0f-bb2c-50944e6e019f]').click();
|
||||||
|
cy.get('.database-calendar').should('exist');
|
||||||
|
cy.wrap(onNavigateToView).should('have.been.calledWith', '2143e95d-5dcb-4e0f-bb2c-50944e6e019f');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function TestDatabase({
|
||||||
|
databaseDoc,
|
||||||
|
rows,
|
||||||
|
folder,
|
||||||
|
iidIndex,
|
||||||
|
initialViewId,
|
||||||
|
onNavigateToView,
|
||||||
|
}: {
|
||||||
|
databaseDoc: YDoc;
|
||||||
|
rows: Y.Map<YDoc>;
|
||||||
|
folder: YFolder;
|
||||||
|
iidIndex: string;
|
||||||
|
initialViewId: string;
|
||||||
|
onNavigateToView: (viewId: string) => void;
|
||||||
|
}) {
|
||||||
|
const [activeViewId, setActiveViewId] = useState<string>(initialViewId);
|
||||||
|
|
||||||
|
const handleNavigateToView = (viewId: string) => {
|
||||||
|
setActiveViewId(viewId);
|
||||||
|
onNavigateToView(viewId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FolderProvider folder={folder}>
|
||||||
|
<IdProvider objectId={iidIndex}>
|
||||||
|
<DatabaseContextProvider
|
||||||
|
viewId={activeViewId || iidIndex}
|
||||||
|
databaseDoc={databaseDoc}
|
||||||
|
rowDocMap={rows}
|
||||||
|
readOnly={true}
|
||||||
|
>
|
||||||
|
<Database iidIndex={iidIndex} viewId={activeViewId} onNavigateToView={handleNavigateToView} />
|
||||||
|
</DatabaseContextProvider>
|
||||||
|
</IdProvider>
|
||||||
|
</FolderProvider>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
import { YDoc, YjsEditorKey } from '@/application/collab.type';
|
||||||
|
import { DatabaseContextState } from '@/application/database-yjs';
|
||||||
|
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||||
|
import { Log } from '@/utils/log';
|
||||||
|
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useGetDatabaseId(iidIndex: string) {
|
||||||
|
const [databaseId, setDatabaseId] = useState<string>();
|
||||||
|
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
||||||
|
|
||||||
|
const loadDatabaseId = useCallback(async () => {
|
||||||
|
if (!databaseService) return;
|
||||||
|
const databases = await databaseService.getWorkspaceDatabases();
|
||||||
|
|
||||||
|
console.log('databses', databases);
|
||||||
|
const id = databases.find((item) => item.views.includes(iidIndex))?.database_id;
|
||||||
|
|
||||||
|
if (!id) return;
|
||||||
|
setDatabaseId(id);
|
||||||
|
}, [iidIndex, databaseService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadDatabaseId();
|
||||||
|
}, [loadDatabaseId]);
|
||||||
|
return databaseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetDatabaseDispatch() {
|
||||||
|
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
||||||
|
const onOpenDatabase = useCallback(
|
||||||
|
async ({ databaseId, rowIds }: { databaseId: string; rowIds?: string[] }) => {
|
||||||
|
if (!databaseService) return Promise.reject();
|
||||||
|
return databaseService.openDatabase(databaseId, rowIds);
|
||||||
|
},
|
||||||
|
[databaseService]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onCloseDatabase = useCallback(
|
||||||
|
(databaseId: string) => {
|
||||||
|
if (!databaseService) return;
|
||||||
|
void databaseService.closeDatabase(databaseId);
|
||||||
|
},
|
||||||
|
[databaseService]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
onOpenDatabase,
|
||||||
|
onCloseDatabase,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLoadDatabase({ databaseId, rowIds }: { databaseId?: string; rowIds?: string[] }) {
|
||||||
|
const [doc, setDoc] = useState<YDoc | null>(null);
|
||||||
|
const [rows, setRows] = useState<DatabaseContextState['rowDocMap'] | null>(null); // Map<rowId, YDoc
|
||||||
|
const [notFound, setNotFound] = useState<boolean>(false);
|
||||||
|
const { onOpenDatabase, onCloseDatabase } = useGetDatabaseDispatch();
|
||||||
|
|
||||||
|
const handleOpenDatabase = useCallback(
|
||||||
|
async (databaseId: string, rowIds?: string[]) => {
|
||||||
|
try {
|
||||||
|
setDoc(null);
|
||||||
|
const { databaseDoc, rows } = await onOpenDatabase({
|
||||||
|
databaseId,
|
||||||
|
rowIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('databaseDoc', databaseDoc.getMap(YjsEditorKey.data_section).toJSON());
|
||||||
|
console.log('rows', rows);
|
||||||
|
|
||||||
|
setDoc(databaseDoc);
|
||||||
|
setRows(rows);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
setNotFound(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onOpenDatabase]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!databaseId) return;
|
||||||
|
void handleOpenDatabase(databaseId, rowIds);
|
||||||
|
return () => {
|
||||||
|
onCloseDatabase(databaseId);
|
||||||
|
};
|
||||||
|
}, [handleOpenDatabase, databaseId, rowIds, onCloseDatabase]);
|
||||||
|
|
||||||
|
return { doc, rows, notFound };
|
||||||
|
}
|
@ -1,103 +1,24 @@
|
|||||||
import { YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
|
||||||
import { DatabaseContextState } from '@/application/database-yjs';
|
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
|
||||||
import RecordNotFound from '@/components/_shared/not-found/RecordNotFound';
|
|
||||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
|
||||||
import DatabaseViews from '@/components/database/DatabaseViews';
|
import DatabaseViews from '@/components/database/DatabaseViews';
|
||||||
import { DatabaseContextProvider } from '@/components/database/DatabaseContext';
|
|
||||||
import { Log } from '@/utils/log';
|
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
|
||||||
import React, { memo, useCallback, useContext, useEffect, useState } from 'react';
|
|
||||||
import { useSearchParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
export const Database = memo((props?: { onNavigateToRow?: (viewId: string, rowId: string) => void }) => {
|
import React, { memo } from 'react';
|
||||||
const { objectId, workspaceId } = useId() || {};
|
|
||||||
const [search, setSearch] = useSearchParams();
|
|
||||||
|
|
||||||
const viewId = search.get('v');
|
export const Database = memo(
|
||||||
const [doc, setDoc] = useState<YDoc | null>(null);
|
({
|
||||||
const [rows, setRows] = useState<DatabaseContextState['rowDocMap'] | null>(null); // Map<rowId, YDoc
|
viewId,
|
||||||
const [notFound, setNotFound] = useState<boolean>(false);
|
onNavigateToView,
|
||||||
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
iidIndex,
|
||||||
|
}: {
|
||||||
const handleOpenDatabase = useCallback(async () => {
|
iidIndex: string;
|
||||||
if (!databaseService || !workspaceId || !objectId) return;
|
viewId: string;
|
||||||
|
onNavigateToView: (viewId: string) => void;
|
||||||
try {
|
}) => {
|
||||||
setDoc(null);
|
console.log('Database', viewId, iidIndex);
|
||||||
const { databaseDoc, rows } = await databaseService.openDatabase(workspaceId, objectId);
|
|
||||||
|
|
||||||
console.log('databaseDoc', databaseDoc.getMap(YjsEditorKey.data_section).toJSON());
|
|
||||||
console.log('rows', rows);
|
|
||||||
|
|
||||||
setDoc(databaseDoc);
|
|
||||||
setRows(rows);
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(e);
|
|
||||||
setNotFound(true);
|
|
||||||
}
|
|
||||||
}, [databaseService, workspaceId, objectId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setNotFound(false);
|
|
||||||
void handleOpenDatabase();
|
|
||||||
}, [handleOpenDatabase]);
|
|
||||||
|
|
||||||
const handleChangeView = useCallback(
|
|
||||||
(viewId: string) => {
|
|
||||||
setSearch({ v: viewId });
|
|
||||||
},
|
|
||||||
[setSearch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const navigateToRow = useCallback(
|
|
||||||
(rowId: string) => {
|
|
||||||
const currentViewId = objectId || viewId;
|
|
||||||
|
|
||||||
if (props?.onNavigateToRow && currentViewId) {
|
|
||||||
props.onNavigateToRow(currentViewId, rowId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSearch({ r: rowId });
|
|
||||||
},
|
|
||||||
[props, setSearch, viewId, objectId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const databaseId = doc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database)?.get(YjsDatabaseKey.id) as string;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!databaseId || !databaseService) return;
|
|
||||||
return () => {
|
|
||||||
void databaseService.closeDatabase(databaseId);
|
|
||||||
};
|
|
||||||
}, [databaseService, databaseId]);
|
|
||||||
|
|
||||||
if (notFound || !objectId) {
|
|
||||||
return <RecordNotFound open={notFound} workspaceId={workspaceId} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rows || !doc) {
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex h-full w-full items-center justify-center'}>
|
<div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'>
|
||||||
<CircularProgress />
|
<DatabaseViews iidIndex={iidIndex} onChangeView={onNavigateToView} viewId={viewId} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return (
|
|
||||||
<div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'>
|
|
||||||
<DatabaseContextProvider
|
|
||||||
navigateToRow={navigateToRow}
|
|
||||||
viewId={viewId || objectId}
|
|
||||||
databaseDoc={doc}
|
|
||||||
rowDocMap={rows}
|
|
||||||
readOnly={true}
|
|
||||||
>
|
|
||||||
<DatabaseViews onChangeView={handleChangeView} currentViewId={viewId || objectId} />
|
|
||||||
</DatabaseContextProvider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Database;
|
export default Database;
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
import { YDoc, YFolder, YjsEditorKey } from '@/application/collab.type';
|
||||||
|
import { applyYDoc } from '@/application/ydoc/apply';
|
||||||
|
import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider';
|
||||||
|
import { IdProvider } from '@/components/_shared/context-provider/IdProvider';
|
||||||
|
import withAppWrapper from '@/components/app/withAppWrapper';
|
||||||
|
import { DatabaseRow } from './DatabaseRow';
|
||||||
|
import { DatabaseContextProvider } from './DatabaseContext';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import '@/components/layout/layout.scss';
|
||||||
|
|
||||||
|
describe('<DatabaseRow />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.viewport(1280, 720);
|
||||||
|
Object.defineProperty(window.navigator, 'language', { value: 'en-US' });
|
||||||
|
Object.defineProperty(window.navigator, 'languages', { value: ['en-US'] });
|
||||||
|
cy.mockDatabase();
|
||||||
|
cy.mockDocument('f56bdf0f-90c8-53fb-97d9-ad5860d2b7a0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with a row', () => {
|
||||||
|
cy.wait(1000);
|
||||||
|
cy.fixture('folder').then((folderJson) => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const state = new Uint8Array(folderJson.data.doc_state);
|
||||||
|
|
||||||
|
applyYDoc(doc, state);
|
||||||
|
const folder = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.folder) as YFolder;
|
||||||
|
|
||||||
|
cy.fixture('database/4c658817-20db-4f56-b7f9-0637a22dfeb6').then((database) => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const databaseState = new Uint8Array(database.data.doc_state);
|
||||||
|
|
||||||
|
applyYDoc(doc, databaseState);
|
||||||
|
|
||||||
|
cy.fixture('database/rows/4c658817-20db-4f56-b7f9-0637a22dfeb6').then((rows) => {
|
||||||
|
const rootRowsDoc = new Y.Doc();
|
||||||
|
const rowsFolder: Y.Map<YDoc> = rootRowsDoc.getMap();
|
||||||
|
const data = rows['2f944220-9f45-40d9-96b5-e8c0888daf7c'];
|
||||||
|
const rowDoc = new Y.Doc();
|
||||||
|
|
||||||
|
applyYDoc(rowDoc, new Uint8Array(data));
|
||||||
|
rowsFolder.set('2f944220-9f45-40d9-96b5-e8c0888daf7c', rowDoc);
|
||||||
|
|
||||||
|
const AppWrapper = withAppWrapper(() => {
|
||||||
|
return (
|
||||||
|
<div className={'flex h-screen w-screen flex-col overflow-y-auto py-4'}>
|
||||||
|
<TestDatabaseRow
|
||||||
|
rowId={'2f944220-9f45-40d9-96b5-e8c0888daf7c'}
|
||||||
|
databaseDoc={doc}
|
||||||
|
rows={rowsFolder}
|
||||||
|
folder={folder}
|
||||||
|
viewId={'7d2148fc-cace-4452-9c5c-96e52e6bf8b5'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.mount(<AppWrapper />);
|
||||||
|
|
||||||
|
cy.wait(1000);
|
||||||
|
|
||||||
|
cy.get('[role="textbox"]').should('exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function TestDatabaseRow({
|
||||||
|
rowId,
|
||||||
|
databaseDoc,
|
||||||
|
rows,
|
||||||
|
folder,
|
||||||
|
viewId,
|
||||||
|
}: {
|
||||||
|
rowId: string;
|
||||||
|
databaseDoc: YDoc;
|
||||||
|
rows: Y.Map<YDoc>;
|
||||||
|
folder: YFolder;
|
||||||
|
viewId: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FolderProvider folder={folder}>
|
||||||
|
<IdProvider objectId={viewId}>
|
||||||
|
<DatabaseContextProvider
|
||||||
|
viewId={viewId}
|
||||||
|
readOnly={true}
|
||||||
|
isDatabaseRowPage
|
||||||
|
databaseDoc={databaseDoc}
|
||||||
|
rowDocMap={rows}
|
||||||
|
>
|
||||||
|
<DatabaseRow rowId={rowId} />
|
||||||
|
</DatabaseContextProvider>
|
||||||
|
</IdProvider>
|
||||||
|
</FolderProvider>
|
||||||
|
);
|
||||||
|
}
|
@ -1,87 +1,25 @@
|
|||||||
import { YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
|
||||||
import { DatabaseContextState } from '@/application/database-yjs';
|
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
|
||||||
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
|
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
|
||||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
|
||||||
import { DatabaseRowProperties, DatabaseRowSubDocument } from '@/components/database/components/database-row';
|
import { DatabaseRowProperties, DatabaseRowSubDocument } from '@/components/database/components/database-row';
|
||||||
import DatabaseRowHeader from '@/components/database/components/header/DatabaseRowHeader';
|
import DatabaseRowHeader from '@/components/database/components/header/DatabaseRowHeader';
|
||||||
import { DatabaseContextProvider } from '@/components/database/DatabaseContext';
|
|
||||||
import { Log } from '@/utils/log';
|
|
||||||
import { Divider } from '@mui/material';
|
import { Divider } from '@mui/material';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import React, { Suspense } from 'react';
|
||||||
import React, { Suspense, useCallback, useContext, useEffect, useState } from 'react';
|
|
||||||
import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound';
|
|
||||||
|
|
||||||
function DatabaseRow({ rowId }: { rowId: string }) {
|
|
||||||
const { objectId, workspaceId } = useId() || {};
|
|
||||||
const [doc, setDoc] = useState<YDoc | null>(null);
|
|
||||||
const [rows, setRows] = useState<DatabaseContextState['rowDocMap'] | null>(null); // Map<rowId, YDoc
|
|
||||||
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
|
||||||
const [notFound, setNotFound] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const handleOpenDatabaseRow = useCallback(async () => {
|
|
||||||
if (!databaseService || !workspaceId || !objectId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setDoc(null);
|
|
||||||
const { databaseDoc, rows } = await databaseService.openDatabase(workspaceId, objectId, [rowId]);
|
|
||||||
|
|
||||||
setDoc(databaseDoc);
|
|
||||||
setRows(rows);
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(e);
|
|
||||||
setNotFound(true);
|
|
||||||
}
|
|
||||||
}, [databaseService, workspaceId, objectId, rowId]);
|
|
||||||
const databaseId = doc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database)?.get(YjsDatabaseKey.id) as string;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setNotFound(false);
|
|
||||||
void handleOpenDatabaseRow();
|
|
||||||
}, [handleOpenDatabaseRow]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!databaseId || !databaseService) return;
|
|
||||||
return () => {
|
|
||||||
void databaseService.closeDatabase(databaseId);
|
|
||||||
};
|
|
||||||
}, [databaseService, databaseId]);
|
|
||||||
|
|
||||||
if (notFound || !objectId) {
|
|
||||||
return <RecordNotFound open={notFound} workspaceId={workspaceId} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rows || !doc) {
|
|
||||||
return (
|
|
||||||
<div className={'flex h-full w-full items-center justify-center'}>
|
|
||||||
<CircularProgress />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export function DatabaseRow({ rowId }: { rowId: string }) {
|
||||||
return (
|
return (
|
||||||
<div className={'flex w-full justify-center'}>
|
<div className={'flex w-full justify-center'}>
|
||||||
<div className={'max-w-screen w-[964px] min-w-0'}>
|
<div className={'max-w-screen w-[964px] min-w-0'}>
|
||||||
<div className={' relative flex flex-col gap-4'}>
|
<div className={' relative flex flex-col gap-4'}>
|
||||||
<DatabaseContextProvider
|
<DatabaseRowHeader rowId={rowId} />
|
||||||
isDatabaseRowPage={true}
|
|
||||||
viewId={objectId}
|
|
||||||
databaseDoc={doc}
|
|
||||||
rowDocMap={rows}
|
|
||||||
readOnly={true}
|
|
||||||
>
|
|
||||||
<DatabaseRowHeader rowId={rowId} />
|
|
||||||
|
|
||||||
<div className={'flex flex-1 flex-col gap-4'}>
|
<div className={'flex flex-1 flex-col gap-4'}>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<DatabaseRowProperties rowId={rowId} />
|
<DatabaseRowProperties rowId={rowId} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Divider className={'mx-16 max-md:mx-4'} />
|
<Divider className={'mx-16 max-md:mx-4'} />
|
||||||
<Suspense fallback={<ComponentLoading />}>
|
<Suspense fallback={<ComponentLoading />}>
|
||||||
<DatabaseRowSubDocument rowId={rowId} />
|
<DatabaseRowSubDocument rowId={rowId} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</DatabaseContextProvider>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,19 +13,21 @@ import DatabaseConditions from 'src/components/database/components/conditions/Da
|
|||||||
|
|
||||||
function DatabaseViews({
|
function DatabaseViews({
|
||||||
onChangeView,
|
onChangeView,
|
||||||
currentViewId,
|
viewId,
|
||||||
|
iidIndex,
|
||||||
}: {
|
}: {
|
||||||
onChangeView: (viewId: string) => void;
|
onChangeView: (viewId: string) => void;
|
||||||
currentViewId: string;
|
viewId: string;
|
||||||
|
iidIndex: string;
|
||||||
}) {
|
}) {
|
||||||
const { childViews, viewIds } = useDatabaseViewsSelector();
|
const { childViews, viewIds } = useDatabaseViewsSelector(iidIndex);
|
||||||
|
|
||||||
const value = useMemo(() => {
|
const value = useMemo(() => {
|
||||||
return Math.max(
|
return Math.max(
|
||||||
0,
|
0,
|
||||||
viewIds.findIndex((id) => id === currentViewId)
|
viewIds.findIndex((id) => id === viewId)
|
||||||
);
|
);
|
||||||
}, [currentViewId, viewIds]);
|
}, [viewId, viewIds]);
|
||||||
|
|
||||||
const [conditionsExpanded, setConditionsExpanded] = useState<boolean>(false);
|
const [conditionsExpanded, setConditionsExpanded] = useState<boolean>(false);
|
||||||
const toggleExpanded = useCallback(() => {
|
const toggleExpanded = useCallback(() => {
|
||||||
@ -58,7 +60,7 @@ function DatabaseViews({
|
|||||||
toggleExpanded,
|
toggleExpanded,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DatabaseTabs selectedViewId={currentViewId} setSelectedViewId={onChangeView} viewIds={viewIds} />
|
<DatabaseTabs selectedViewId={viewId} setSelectedViewId={onChangeView} viewIds={viewIds} />
|
||||||
<DatabaseConditions />
|
<DatabaseConditions />
|
||||||
</DatabaseConditionsContext.Provider>
|
</DatabaseConditionsContext.Provider>
|
||||||
<div className={'flex h-full w-full flex-1 flex-col overflow-hidden'}>
|
<div className={'flex h-full w-full flex-1 flex-col overflow-hidden'}>
|
||||||
|
@ -16,7 +16,7 @@ export function Board() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'grid-board flex w-full flex-1 flex-col'}>
|
<div className={'database-board flex w-full flex-1 flex-col'}>
|
||||||
{groups.map((groupId) => (
|
{groups.map((groupId) => (
|
||||||
<Group key={groupId} groupId={groupId} />
|
<Group key={groupId} groupId={groupId} />
|
||||||
))}
|
))}
|
||||||
|
@ -8,7 +8,7 @@ export function Calendar() {
|
|||||||
const { dayPropGetter, localizer, formats, events, emptyEvents } = useCalendarSetup();
|
const { dayPropGetter, localizer, formats, events, emptyEvents } = useCalendarSetup();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'appflowy-calendar h-full max-h-[960px] px-16 pt-4 max-md:px-4'}>
|
<div className={'database-calendar h-full max-h-[960px] px-16 pt-4 max-md:px-4'}>
|
||||||
<BigCalendar
|
<BigCalendar
|
||||||
components={{
|
components={{
|
||||||
toolbar: (props) => <Toolbar {...props} emptyEvents={emptyEvents} />,
|
toolbar: (props) => <Toolbar {...props} emptyEvents={emptyEvents} />,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@use "src/styles/mixin.scss";
|
||||||
|
|
||||||
$today-highlight-bg: transparent;
|
$today-highlight-bg: transparent;
|
||||||
@import 'react-big-calendar/lib/sass/styles';
|
@import 'react-big-calendar/lib/sass/styles';
|
||||||
@import 'react-big-calendar/lib/addons/dragAndDrop/styles'; // if using DnD
|
@import 'react-big-calendar/lib/addons/dragAndDrop/styles'; // if using DnD
|
||||||
@ -34,20 +36,7 @@ $today-highlight-bg: transparent;
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
@include mixin.scrollbar-style;
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb, & *::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--scrollbar-thumb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { useFieldsSelector, useNavigateToRow } from '@/application/database-yjs';
|
import { useFieldsSelector, useNavigateToRow } from '@/application/database-yjs';
|
||||||
import OpenAction from '@/components/database/components/database-row/OpenAction';
|
|
||||||
import CardField from '@/components/database/components/field/CardField';
|
import CardField from '@/components/database/components/field/CardField';
|
||||||
import { getPlatform } from '@/utils/platform';
|
import React, { memo, useEffect, useMemo } from 'react';
|
||||||
import React, { useEffect, useMemo } from 'react';
|
|
||||||
|
|
||||||
export interface CardProps {
|
export interface CardProps {
|
||||||
groupFieldId: string;
|
groupFieldId: string;
|
||||||
@ -11,11 +9,10 @@ export interface CardProps {
|
|||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Card({ groupFieldId, rowId, onResize, isDragging }: CardProps) {
|
export const Card = memo(({ groupFieldId, rowId, onResize, isDragging }: CardProps) => {
|
||||||
const fields = useFieldsSelector();
|
const fields = useFieldsSelector();
|
||||||
const showFields = useMemo(() => fields.filter((field) => field.fieldId !== groupFieldId), [fields, groupFieldId]);
|
const showFields = useMemo(() => fields.filter((field) => field.fieldId !== groupFieldId), [fields, groupFieldId]);
|
||||||
|
|
||||||
const [isHovering, setIsHovering] = React.useState(false);
|
|
||||||
const ref = React.useRef<HTMLDivElement | null>(null);
|
const ref = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -35,35 +32,24 @@ export function Card({ groupFieldId, rowId, onResize, isDragging }: CardProps) {
|
|||||||
};
|
};
|
||||||
}, [onResize, isDragging]);
|
}, [onResize, isDragging]);
|
||||||
|
|
||||||
const isMobile = useMemo(() => {
|
|
||||||
return getPlatform().isMobile;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const navigateToRow = useNavigateToRow();
|
const navigateToRow = useNavigateToRow();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMobile) {
|
navigateToRow?.(rowId);
|
||||||
navigateToRow?.(rowId);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onMouseEnter={() => setIsHovering(true)}
|
|
||||||
onMouseLeave={() => setIsHovering(false)}
|
|
||||||
style={{
|
style={{
|
||||||
minHeight: '38px',
|
minHeight: '38px',
|
||||||
}}
|
}}
|
||||||
className='relative flex cursor-pointer flex-col rounded-lg border border-line-divider p-3 text-xs shadow-sm hover:bg-fill-list-active hover:shadow'
|
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'
|
||||||
>
|
>
|
||||||
{showFields.map((field, index) => {
|
{showFields.map((field, index) => {
|
||||||
return <CardField index={index} key={field.fieldId} rowId={rowId} fieldId={field.fieldId} />;
|
return <CardField index={index} key={field.fieldId} rowId={rowId} fieldId={field.fieldId} />;
|
||||||
})}
|
})}
|
||||||
<div className={`absolute top-1.5 right-1.5 ${isHovering ? 'block' : 'hidden'}`}>
|
|
||||||
<OpenAction rowId={rowId} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export default Card;
|
export default Card;
|
||||||
|
@ -4,7 +4,7 @@ import { Tag } from '@/components/_shared/tag';
|
|||||||
import ListItem from '@/components/database/components/board/column/ListItem';
|
import ListItem from '@/components/database/components/board/column/ListItem';
|
||||||
import { useRenderColumn } from '@/components/database/components/board/column/useRenderColumn';
|
import { useRenderColumn } from '@/components/database/components/board/column/useRenderColumn';
|
||||||
import { useMeasureHeight } from '@/components/database/components/cell/useMeasure';
|
import { useMeasureHeight } from '@/components/database/components/cell/useMeasure';
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { memo, useCallback, useEffect, useMemo } from 'react';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { VariableSizeList } from 'react-window';
|
import { VariableSizeList } from 'react-window';
|
||||||
|
|
||||||
@ -14,86 +14,89 @@ export interface ColumnProps {
|
|||||||
fieldId: string;
|
fieldId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Column({ id, rows, fieldId }: ColumnProps) {
|
export const Column = memo(
|
||||||
const { header } = useRenderColumn(id, fieldId);
|
({ id, rows, fieldId }: ColumnProps) => {
|
||||||
const ref = React.useRef<VariableSizeList | null>(null);
|
const { header } = useRenderColumn(id, fieldId);
|
||||||
const forceUpdate = useCallback((index: number) => {
|
const ref = React.useRef<VariableSizeList | null>(null);
|
||||||
ref.current?.resetAfterIndex(index, true);
|
const forceUpdate = useCallback((index: number) => {
|
||||||
}, []);
|
ref.current?.resetAfterIndex(index, true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
forceUpdate(0);
|
forceUpdate(0);
|
||||||
}, [rows, forceUpdate]);
|
}, [rows, forceUpdate]);
|
||||||
|
|
||||||
const measureRows = useMemo(
|
const measureRows = useMemo(
|
||||||
() =>
|
() =>
|
||||||
rows?.map((row) => {
|
rows?.map((row) => {
|
||||||
return {
|
return {
|
||||||
rowId: row.id,
|
rowId: row.id,
|
||||||
|
};
|
||||||
|
}) || [],
|
||||||
|
[rows]
|
||||||
|
);
|
||||||
|
const { rowHeight, onResize } = useMeasureHeight({ forceUpdate, rows: measureRows });
|
||||||
|
|
||||||
|
const Row = useCallback(
|
||||||
|
({ index, style, data }: { index: number; style: React.CSSProperties; data: Row[] }) => {
|
||||||
|
const item = data[index];
|
||||||
|
|
||||||
|
// We are rendering an extra item for the placeholder
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onResizeCallback = (height: number) => {
|
||||||
|
onResize(index, 0, {
|
||||||
|
width: 0,
|
||||||
|
height: height + 8,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}) || [],
|
|
||||||
[rows]
|
|
||||||
);
|
|
||||||
const { rowHeight, onResize } = useMeasureHeight({ forceUpdate, rows: measureRows });
|
|
||||||
|
|
||||||
const Row = useCallback(
|
return <ListItem fieldId={fieldId} onResize={onResizeCallback} item={item} style={style} />;
|
||||||
({ index, style, data }: { index: number; style: React.CSSProperties; data: Row[] }) => {
|
},
|
||||||
const item = data[index];
|
[fieldId, onResize]
|
||||||
|
);
|
||||||
|
|
||||||
// We are rendering an extra item for the placeholder
|
const getItemSize = useCallback(
|
||||||
if (!item) {
|
(index: number) => {
|
||||||
return null;
|
if (!rows || index >= rows.length) return 0;
|
||||||
}
|
const row = rows[index];
|
||||||
|
|
||||||
const onResizeCallback = (height: number) => {
|
if (!row) return 0;
|
||||||
onResize(index, 0, {
|
return rowHeight(index);
|
||||||
width: 0,
|
},
|
||||||
height: height + 8,
|
[rowHeight, rows]
|
||||||
});
|
);
|
||||||
};
|
const rowCount = rows?.length || 0;
|
||||||
|
|
||||||
return <ListItem fieldId={fieldId} onResize={onResizeCallback} item={item} style={style} />;
|
return (
|
||||||
},
|
<div key={id} className='column flex w-[230px] flex-col gap-4'>
|
||||||
[fieldId, onResize]
|
<div className='column-header flex h-[24px] items-center text-xs font-medium'>
|
||||||
);
|
<Tag label={header?.name} color={header?.color} />
|
||||||
|
</div>
|
||||||
|
|
||||||
const getItemSize = useCallback(
|
<div className={'w-full flex-1 overflow-hidden'}>
|
||||||
(index: number) => {
|
<AutoSizer>
|
||||||
if (!rows || index >= rows.length) return 0;
|
{({ height, width }: { height: number; width: number }) => {
|
||||||
const row = rows[index];
|
return (
|
||||||
|
<VariableSizeList
|
||||||
if (!row) return 0;
|
ref={ref}
|
||||||
return rowHeight(index);
|
height={height}
|
||||||
},
|
itemCount={rowCount}
|
||||||
[rowHeight, rows]
|
itemSize={getItemSize}
|
||||||
);
|
width={width}
|
||||||
const rowCount = rows?.length || 0;
|
outerElementType={AFScroller}
|
||||||
|
itemData={rows}
|
||||||
return (
|
>
|
||||||
<div key={id} className='column flex w-[230px] flex-col gap-4'>
|
{Row}
|
||||||
<div className='column-header flex h-[24px] items-center text-xs font-medium'>
|
</VariableSizeList>
|
||||||
<Tag label={header?.name} color={header?.color} />
|
);
|
||||||
|
}}
|
||||||
|
</AutoSizer>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
<div className={'w-full flex-1 overflow-hidden'}>
|
},
|
||||||
<AutoSizer>
|
(prev, next) => JSON.stringify(prev) === JSON.stringify(next)
|
||||||
{({ height, width }: { height: number; width: number }) => {
|
);
|
||||||
return (
|
|
||||||
<VariableSizeList
|
|
||||||
ref={ref}
|
|
||||||
height={height}
|
|
||||||
itemCount={rowCount}
|
|
||||||
itemSize={getItemSize}
|
|
||||||
width={width}
|
|
||||||
outerElementType={AFScroller}
|
|
||||||
itemData={rows}
|
|
||||||
>
|
|
||||||
{Row}
|
|
||||||
</VariableSizeList>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -1,29 +1,33 @@
|
|||||||
import { Row } from '@/application/database-yjs';
|
import { Row } from '@/application/database-yjs';
|
||||||
import React from 'react';
|
import React, { memo } from 'react';
|
||||||
|
import { areEqual } from 'react-window';
|
||||||
import Card from 'src/components/database/components/board/card/Card';
|
import Card from 'src/components/database/components/board/card/Card';
|
||||||
|
|
||||||
export const ListItem = ({
|
export const ListItem = memo(
|
||||||
item,
|
({
|
||||||
style,
|
item,
|
||||||
onResize,
|
style,
|
||||||
fieldId,
|
onResize,
|
||||||
}: {
|
fieldId,
|
||||||
item?: Row;
|
}: {
|
||||||
style?: React.CSSProperties;
|
item?: Row;
|
||||||
fieldId: string;
|
style?: React.CSSProperties;
|
||||||
onResize?: (height: number) => void;
|
fieldId: string;
|
||||||
}) => {
|
onResize?: (height: number) => void;
|
||||||
return (
|
}) => {
|
||||||
<div
|
return (
|
||||||
style={{
|
<div
|
||||||
...style,
|
style={{
|
||||||
width: 'calc(100% - 2px)',
|
...style,
|
||||||
}}
|
width: 'calc(100% - 2px)',
|
||||||
className={`w-full bg-bg-body`}
|
}}
|
||||||
>
|
className={`w-full bg-bg-body`}
|
||||||
{item?.id ? <Card onResize={onResize} rowId={item.id} groupFieldId={fieldId} /> : null}
|
>
|
||||||
</div>
|
{item?.id ? <Card onResize={onResize} rowId={item.id} groupFieldId={fieldId} /> : null}
|
||||||
);
|
</div>
|
||||||
};
|
);
|
||||||
|
},
|
||||||
|
areEqual
|
||||||
|
);
|
||||||
|
|
||||||
export default ListItem;
|
export default ListItem;
|
||||||
|
@ -6,29 +6,27 @@ import {
|
|||||||
useFieldSelector,
|
useFieldSelector,
|
||||||
useNavigateToRow,
|
useNavigateToRow,
|
||||||
} from '@/application/database-yjs';
|
} from '@/application/database-yjs';
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
|
||||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
|
||||||
import { RelationCell, RelationCellData } from '@/components/database/components/cell/cell.type';
|
import { RelationCell, RelationCellData } from '@/components/database/components/cell/cell.type';
|
||||||
import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue';
|
import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue';
|
||||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
import { useGetDatabaseDispatch } from '@/components/database/Database.hooks';
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) {
|
function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) {
|
||||||
const { field } = useFieldSelector(fieldId);
|
const { field } = useFieldSelector(fieldId);
|
||||||
const currentDatabaseId = useDatabase()?.get(YjsDatabaseKey.id);
|
const currentDatabaseId = useDatabase()?.get(YjsDatabaseKey.id);
|
||||||
const workspaceId = useId()?.workspaceId;
|
const { onOpenDatabase, onCloseDatabase } = useGetDatabaseDispatch();
|
||||||
const rowIds = useMemo(() => {
|
const rowIds = useMemo(() => {
|
||||||
return (cell.data?.toJSON() as RelationCellData) ?? [];
|
return (cell.data?.toJSON() as RelationCellData) ?? [];
|
||||||
}, [cell.data]);
|
}, [cell.data]);
|
||||||
const databaseId = rowIds.length > 0 && field ? parseRelationTypeOption(field).database_id : undefined;
|
const databaseId = rowIds.length > 0 && field ? parseRelationTypeOption(field).database_id : undefined;
|
||||||
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
|
||||||
const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState<string | undefined>(undefined);
|
const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState<string | undefined>(undefined);
|
||||||
const [rows, setRows] = useState<DatabaseContextState['rowDocMap'] | null>();
|
const [rows, setRows] = useState<DatabaseContextState['rowDocMap'] | null>();
|
||||||
|
|
||||||
const navigateToRow = useNavigateToRow();
|
const navigateToRow = useNavigateToRow();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workspaceId || !databaseId || !rowIds.length) return;
|
if (!databaseId || !rowIds.length) return;
|
||||||
void databaseService?.getDatabase(workspaceId, databaseId, rowIds).then(({ databaseDoc: doc, rows }) => {
|
void onOpenDatabase({ databaseId, rowIds }).then(({ databaseDoc: doc, rows }) => {
|
||||||
const fields = doc
|
const fields = doc
|
||||||
.getMap(YjsEditorKey.data_section)
|
.getMap(YjsEditorKey.data_section)
|
||||||
.get(YjsEditorKey.database)
|
.get(YjsEditorKey.database)
|
||||||
@ -42,15 +40,15 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId:
|
|||||||
|
|
||||||
setRows(rows);
|
setRows(rows);
|
||||||
});
|
});
|
||||||
}, [workspaceId, databaseId, databaseService, rowIds]);
|
}, [onOpenDatabase, databaseId, rowIds, onCloseDatabase]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (currentDatabaseId !== databaseId && databaseId) {
|
if (currentDatabaseId !== databaseId && databaseId) {
|
||||||
void databaseService?.closeDatabase(databaseId);
|
onCloseDatabase(databaseId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [currentDatabaseId, databaseId, databaseService]);
|
}, [databaseId, currentDatabaseId, onCloseDatabase]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style} className={'relation-cell flex w-full items-center gap-2'}>
|
<div style={style} className={'relation-cell flex w-full items-center gap-2'}>
|
||||||
|
@ -1,27 +1,24 @@
|
|||||||
import { YDoc } from '@/application/collab.type';
|
import { YDoc } from '@/application/collab.type';
|
||||||
import { useRowMetaSelector } from '@/application/database-yjs';
|
import { useRowMetaSelector } from '@/application/database-yjs';
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
|
||||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||||
import { Editor } from '@/components/editor';
|
import { Editor } from '@/components/editor';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
export function DatabaseRowSubDocument({ rowId }: { rowId: string }) {
|
export function DatabaseRowSubDocument({ rowId }: { rowId: string }) {
|
||||||
const { workspaceId } = useId() || {};
|
|
||||||
const meta = useRowMetaSelector(rowId);
|
const meta = useRowMetaSelector(rowId);
|
||||||
const documentId = meta?.documentId;
|
const documentId = meta?.documentId;
|
||||||
|
|
||||||
console.log('documentId', documentId);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [doc, setDoc] = useState<YDoc | null>(null);
|
const [doc, setDoc] = useState<YDoc | null>(null);
|
||||||
|
|
||||||
const documentService = useContext(AFConfigContext)?.service?.documentService;
|
const documentService = useContext(AFConfigContext)?.service?.documentService;
|
||||||
|
|
||||||
const handleOpenDocument = useCallback(async () => {
|
const handleOpenDocument = useCallback(async () => {
|
||||||
if (!documentService || !workspaceId || !documentId) return;
|
if (!documentService || !documentId) return;
|
||||||
try {
|
try {
|
||||||
setDoc(null);
|
setDoc(null);
|
||||||
const doc = await documentService.openDocument(workspaceId, documentId);
|
const doc = await documentService.openDocument(documentId);
|
||||||
|
|
||||||
console.log('doc', doc);
|
console.log('doc', doc);
|
||||||
setDoc(doc);
|
setDoc(doc);
|
||||||
@ -29,7 +26,7 @@ export function DatabaseRowSubDocument({ rowId }: { rowId: string }) {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
// haven't created by client, ignore error and show empty
|
// haven't created by client, ignore error and show empty
|
||||||
}
|
}
|
||||||
}, [documentService, workspaceId, documentId]);
|
}, [documentService, documentId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
import React, { memo, useEffect, useRef } from 'react';
|
||||||
import { GridChildComponentProps, VariableSizeGrid } from 'react-window';
|
import { areEqual, GridChildComponentProps, VariableSizeGrid } from 'react-window';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { GridColumnType, RenderColumn, GridColumn } from '../grid-column';
|
import { GridColumnType, RenderColumn, GridColumn } from '../grid-column';
|
||||||
|
|
||||||
@ -10,24 +10,25 @@ export interface GridHeaderProps {
|
|||||||
scrollLeft?: number;
|
scrollLeft?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Cell = memo(({ columnIndex, style, data }: GridChildComponentProps) => {
|
||||||
|
const column = data[columnIndex];
|
||||||
|
|
||||||
|
// Placeholder for Action toolbar
|
||||||
|
if (!column || column.type === GridColumnType.Action) return <div style={style} />;
|
||||||
|
|
||||||
|
if (column.type === GridColumnType.Field) {
|
||||||
|
return (
|
||||||
|
<div style={style}>
|
||||||
|
<GridColumn column={column} index={columnIndex} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div style={style} className={'border-t border-b border-line-divider'} />;
|
||||||
|
}, areEqual);
|
||||||
|
|
||||||
export const GridHeader = ({ scrollLeft, onScrollLeft, columnWidth, columns }: GridHeaderProps) => {
|
export const GridHeader = ({ scrollLeft, onScrollLeft, columnWidth, columns }: GridHeaderProps) => {
|
||||||
const ref = useRef<VariableSizeGrid | null>(null);
|
const ref = useRef<VariableSizeGrid | null>(null);
|
||||||
const Cell = useCallback(({ columnIndex, style, data }: GridChildComponentProps) => {
|
|
||||||
const column = data[columnIndex];
|
|
||||||
|
|
||||||
// Placeholder for Action toolbar
|
|
||||||
if (!column || column.type === GridColumnType.Action) return <div style={style} />;
|
|
||||||
|
|
||||||
if (column.type === GridColumnType.Field) {
|
|
||||||
return (
|
|
||||||
<div style={style}>
|
|
||||||
<GridColumn column={column} index={columnIndex} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div style={style} className={'border-t border-b border-line-divider'} />;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
import { areEqual } from 'react-window';
|
||||||
import { GridColumnType } from '../grid-column';
|
import { GridColumnType } from '../grid-column';
|
||||||
import React from 'react';
|
import React, { memo } from 'react';
|
||||||
import GridCell from '../grid-cell/GridCell';
|
import GridCell from '../grid-cell/GridCell';
|
||||||
|
|
||||||
export interface GridRowCellProps {
|
export interface GridRowCellProps {
|
||||||
@ -11,7 +12,7 @@ export interface GridRowCellProps {
|
|||||||
onResize?: (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => void;
|
onResize?: (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GridRowCell({ onResize, rowIndex, columnIndex, rowId, fieldId, type }: GridRowCellProps) {
|
export const GridRowCell = memo(({ onResize, rowIndex, columnIndex, rowId, fieldId, type }: GridRowCellProps) => {
|
||||||
if (type === GridColumnType.Field && fieldId) {
|
if (type === GridColumnType.Field && fieldId) {
|
||||||
return (
|
return (
|
||||||
<GridCell rowIndex={rowIndex} onResize={onResize} rowId={rowId} fieldId={fieldId} columnIndex={columnIndex} />
|
<GridCell rowIndex={rowIndex} onResize={onResize} rowId={rowId} fieldId={fieldId} columnIndex={columnIndex} />
|
||||||
@ -23,6 +24,6 @@ export function GridRowCell({ onResize, rowIndex, columnIndex, rowId, fieldId, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}, areEqual);
|
||||||
|
|
||||||
export default GridRowCell;
|
export default GridRowCell;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const';
|
|
||||||
import { AFScroller } from '@/components/_shared/scroller';
|
import { AFScroller } from '@/components/_shared/scroller';
|
||||||
|
import { useMeasureHeight } from '@/components/database/components/cell/useMeasure';
|
||||||
import { GridColumnType, RenderColumn } from '../grid-column';
|
import { GridColumnType, RenderColumn } from '../grid-column';
|
||||||
import { GridCalculateRowCell, GridRowCell, RenderRowType, useRenderRows } from '../grid-row';
|
import { GridCalculateRowCell, GridRowCell, RenderRowType, useRenderRows } from '../grid-row';
|
||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
@ -18,7 +18,11 @@ export interface GridTableProps {
|
|||||||
export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: GridTableProps) => {
|
export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: GridTableProps) => {
|
||||||
const ref = useRef<VariableSizeGrid | null>(null);
|
const ref = useRef<VariableSizeGrid | null>(null);
|
||||||
const { rows } = useRenderRows();
|
const { rows } = useRenderRows();
|
||||||
const rowHeights = useRef<{ [key: string]: number }>({});
|
const forceUpdate = useCallback((index: number) => {
|
||||||
|
ref.current?.resetAfterRowIndex(index, true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { rowHeight, onResize } = useMeasureHeight({ forceUpdate, rows });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
@ -32,40 +36,6 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr
|
|||||||
}
|
}
|
||||||
}, [columns]);
|
}, [columns]);
|
||||||
|
|
||||||
const rowHeight = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
const row = rows[index];
|
|
||||||
|
|
||||||
if (!row || !row.rowId) return DEFAULT_ROW_HEIGHT;
|
|
||||||
|
|
||||||
return rowHeights.current[row.rowId] || DEFAULT_ROW_HEIGHT;
|
|
||||||
},
|
|
||||||
[rows]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setRowHeight = useCallback(
|
|
||||||
(index: number, height: number) => {
|
|
||||||
const row = rows[index];
|
|
||||||
const rowId = row.rowId;
|
|
||||||
|
|
||||||
if (!row || !rowId) return;
|
|
||||||
const oldHeight = rowHeights.current[rowId];
|
|
||||||
|
|
||||||
rowHeights.current[rowId] = Math.max(oldHeight || DEFAULT_ROW_HEIGHT, height);
|
|
||||||
if (oldHeight !== height) {
|
|
||||||
ref.current?.resetAfterRowIndex(index, true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[rows]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onResize = useCallback(
|
|
||||||
(rowIndex: number, columnIndex: number, size: { width: number; height: number }) => {
|
|
||||||
setRowHeight(rowIndex, size.height);
|
|
||||||
},
|
|
||||||
[setRowHeight]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getItemKey = useCallback(
|
const getItemKey = useCallback(
|
||||||
({ columnIndex, rowIndex }: { columnIndex: number; rowIndex: number }) => {
|
({ columnIndex, rowIndex }: { columnIndex: number; rowIndex: number }) => {
|
||||||
const row = rows[rowIndex];
|
const row = rows[rowIndex];
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { DatabaseViewLayout, ViewLayout, YjsDatabaseKey, YjsFolderKey, YView } from '@/application/collab.type';
|
import { DatabaseViewLayout, ViewLayout, YjsDatabaseKey, YjsFolderKey, YView } from '@/application/collab.type';
|
||||||
import { useDatabaseView } from '@/application/database-yjs';
|
import { useDatabaseView } from '@/application/database-yjs';
|
||||||
import { useFolderContext } from '@/application/folder-yjs';
|
import { useFolderContext } from '@/application/folder-yjs';
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
|
||||||
import { DatabaseActions } from '@/components/database/components/conditions';
|
import { DatabaseActions } from '@/components/database/components/conditions';
|
||||||
import { Tooltip } from '@mui/material';
|
import { Tooltip } from '@mui/material';
|
||||||
import { forwardRef, FunctionComponent, SVGProps, useCallback, useEffect, useMemo } from 'react';
|
import { forwardRef, FunctionComponent, SVGProps, useCallback, useMemo } from 'react';
|
||||||
import { ViewTabs, ViewTab } from './ViewTabs';
|
import { ViewTabs, ViewTab } from './ViewTabs';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -30,7 +29,6 @@ const DatabaseIcons: {
|
|||||||
|
|
||||||
export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
||||||
({ viewIds, selectedViewId, setSelectedViewId }, ref) => {
|
({ viewIds, selectedViewId, setSelectedViewId }, ref) => {
|
||||||
const objectId = useId().objectId;
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const folder = useFolderContext();
|
const folder = useFolderContext();
|
||||||
const view = useDatabaseView();
|
const view = useDatabaseView();
|
||||||
@ -40,13 +38,6 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
setSelectedViewId?.(newValue);
|
setSelectedViewId?.(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedViewId === undefined) {
|
|
||||||
setSelectedViewId?.(objectId);
|
|
||||||
}
|
|
||||||
}, [selectedViewId, setSelectedViewId, objectId]);
|
|
||||||
const isSelected = useMemo(() => viewIds.some((viewId) => viewId === selectedViewId), [viewIds, selectedViewId]);
|
|
||||||
|
|
||||||
const getFolderView = useCallback(
|
const getFolderView = useCallback(
|
||||||
(viewId: string) => {
|
(viewId: string) => {
|
||||||
if (!folder) return null;
|
if (!folder) return null;
|
||||||
@ -80,7 +71,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
scrollButtons={false}
|
scrollButtons={false}
|
||||||
variant='scrollable'
|
variant='scrollable'
|
||||||
allowScrollButtonsMobile
|
allowScrollButtonsMobile
|
||||||
value={isSelected ? selectedViewId : objectId}
|
value={selectedViewId}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{viewIds.map((viewId) => {
|
{viewIds.map((viewId) => {
|
||||||
@ -94,11 +85,12 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
return (
|
return (
|
||||||
<ViewTab
|
<ViewTab
|
||||||
key={viewId}
|
key={viewId}
|
||||||
|
data-testid={`view-tab-${viewId}`}
|
||||||
icon={<Icon className={'h-4 w-4'} />}
|
icon={<Icon className={'h-4 w-4'} />}
|
||||||
iconPosition='start'
|
iconPosition='start'
|
||||||
color='inherit'
|
color='inherit'
|
||||||
label={
|
label={
|
||||||
<Tooltip title={name} placement={'right'}>
|
<Tooltip title={name} enterDelay={1000} enterNextDelay={1000} placement={'right'}>
|
||||||
<span className={'max-w-[120px] truncate'}>{name || t('grid.title.placeholder')}</span>
|
<span className={'max-w-[120px] truncate'}>{name || t('grid.title.placeholder')}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ export function Grid() {
|
|||||||
rowOrders,
|
rowOrders,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={'flex w-full flex-1 flex-col'}>
|
<div className={'database-grid flex w-full flex-1 flex-col'}>
|
||||||
<GridHeader scrollLeft={scrollLeft} columnWidth={columnWidth} columns={fields} onScrollLeft={setScrollLeft} />
|
<GridHeader scrollLeft={scrollLeft} columnWidth={columnWidth} columns={fields} onScrollLeft={setScrollLeft} />
|
||||||
<div className={'grid-scroll-table w-full flex-1'}>
|
<div className={'grid-scroll-table w-full flex-1'}>
|
||||||
<GridTable
|
<GridTable
|
||||||
|
@ -12,7 +12,7 @@ import React, { Suspense, useCallback, useContext, useEffect, useMemo, useState
|
|||||||
import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound';
|
import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound';
|
||||||
|
|
||||||
export const Document = () => {
|
export const Document = () => {
|
||||||
const { objectId: documentId, workspaceId } = useId() || {};
|
const { objectId: documentId } = useId() || {};
|
||||||
const [doc, setDoc] = useState<YDoc | null>(null);
|
const [doc, setDoc] = useState<YDoc | null>(null);
|
||||||
const [notFound, setNotFound] = useState<boolean>(false);
|
const [notFound, setNotFound] = useState<boolean>(false);
|
||||||
const extra = usePageInfo(documentId).extra;
|
const extra = usePageInfo(documentId).extra;
|
||||||
@ -27,17 +27,17 @@ export const Document = () => {
|
|||||||
const documentService = useContext(AFConfigContext)?.service?.documentService;
|
const documentService = useContext(AFConfigContext)?.service?.documentService;
|
||||||
|
|
||||||
const handleOpenDocument = useCallback(async () => {
|
const handleOpenDocument = useCallback(async () => {
|
||||||
if (!documentService || !workspaceId || !documentId) return;
|
if (!documentService || !documentId) return;
|
||||||
try {
|
try {
|
||||||
setDoc(null);
|
setDoc(null);
|
||||||
const doc = await documentService.openDocument(workspaceId, documentId);
|
const doc = await documentService.openDocument(documentId);
|
||||||
|
|
||||||
setDoc(doc);
|
setDoc(doc);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
setNotFound(true);
|
setNotFound(true);
|
||||||
}
|
}
|
||||||
}, [documentService, workspaceId, documentId]);
|
}, [documentService, documentId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNotFound(false);
|
setNotFound(false);
|
||||||
@ -105,7 +105,7 @@ export const Document = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<RecordNotFound open={notFound} workspaceId={workspaceId} />
|
<RecordNotFound open={notFound} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,25 +1,21 @@
|
|||||||
import { DocCoverType, YDoc } from '@/application/collab.type';
|
|
||||||
import { CoverType } from '@/application/folder-yjs/folder.type';
|
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
|
||||||
import { usePageInfo } from '@/components/_shared/page/usePageInfo';
|
|
||||||
import { useBlockCover } from '@/components/document/document_header/useBlockCover';
|
|
||||||
import { showColorsForImage } from '@/components/document/document_header/utils';
|
import { showColorsForImage } from '@/components/document/document_header/utils';
|
||||||
import { renderColor } from '@/utils/color';
|
import { renderColor } from '@/utils/color';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import DefaultImage from './default_cover.jpg';
|
|
||||||
|
|
||||||
function DocumentCover({ doc, onTextColor }: { doc: YDoc; onTextColor: (color: string) => void }) {
|
|
||||||
const viewId = useId().objectId;
|
|
||||||
const { extra } = usePageInfo(viewId);
|
|
||||||
|
|
||||||
const pageCover = extra.cover;
|
|
||||||
const { cover } = useBlockCover(doc);
|
|
||||||
|
|
||||||
|
function DocumentCover({
|
||||||
|
coverValue,
|
||||||
|
coverType,
|
||||||
|
onTextColor,
|
||||||
|
}: {
|
||||||
|
coverValue?: string;
|
||||||
|
coverType?: string;
|
||||||
|
onTextColor: (color: string) => void;
|
||||||
|
}) {
|
||||||
const renderCoverColor = useCallback((color: string) => {
|
const renderCoverColor = useCallback((color: string) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: renderColor(color),
|
background: renderColor(color),
|
||||||
}}
|
}}
|
||||||
className={`h-full w-full`}
|
className={`h-full w-full`}
|
||||||
/>
|
/>
|
||||||
@ -45,26 +41,14 @@ function DocumentCover({ doc, onTextColor }: { doc: YDoc; onTextColor: (color: s
|
|||||||
[onTextColor]
|
[onTextColor]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!pageCover && !cover?.cover_selection) return null;
|
if (!coverType || !coverValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative flex h-[255px] w-full max-sm:h-[180px]`}>
|
<div className={'relative flex h-[255px] w-full max-sm:h-[180px]'}>
|
||||||
{pageCover ? (
|
{coverType === 'color' && renderCoverColor(coverValue)}
|
||||||
<>
|
{(coverType === 'custom' || coverType === 'built_in') && renderCoverImage(coverValue)}
|
||||||
{[CoverType.NormalColor, CoverType.GradientColor].includes(pageCover.type)
|
|
||||||
? renderCoverColor(pageCover.value)
|
|
||||||
: null}
|
|
||||||
{CoverType.BuildInImage === pageCover.type ? renderCoverImage(DefaultImage) : null}
|
|
||||||
{[CoverType.CustomImage, CoverType.UpsplashImage].includes(pageCover.type)
|
|
||||||
? renderCoverImage(pageCover.value)
|
|
||||||
: null}
|
|
||||||
</>
|
|
||||||
) : cover?.cover_selection ? (
|
|
||||||
<>
|
|
||||||
{cover.cover_selection_type === DocCoverType.Asset ? renderCoverImage(DefaultImage) : null}
|
|
||||||
{cover.cover_selection_type === DocCoverType.Color ? renderCoverColor(cover.cover_selection) : null}
|
|
||||||
{cover.cover_selection_type === DocCoverType.Image ? renderCoverImage(cover.cover_selection) : null}
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
import { YDoc, YjsFolderKey } from '@/application/collab.type';
|
import { DocCoverType, YDoc, YjsFolderKey } from '@/application/collab.type';
|
||||||
import { useViewSelector } from '@/application/folder-yjs';
|
import { useViewSelector } from '@/application/folder-yjs';
|
||||||
|
import { CoverType } from '@/application/folder-yjs/folder.type';
|
||||||
|
import { usePageInfo } from '@/components/_shared/page/usePageInfo';
|
||||||
import DocumentCover from '@/components/document/document_header/DocumentCover';
|
import DocumentCover from '@/components/document/document_header/DocumentCover';
|
||||||
|
import { useBlockCover } from '@/components/document/document_header/useBlockCover';
|
||||||
import React, { memo, useMemo, useRef, useState } from 'react';
|
import React, { memo, useMemo, useRef, useState } from 'react';
|
||||||
|
import BuiltInImage1 from '@/assets/cover/m_cover_image_1.png';
|
||||||
|
import BuiltInImage2 from '@/assets/cover/m_cover_image_2.png';
|
||||||
|
import BuiltInImage3 from '@/assets/cover/m_cover_image_3.png';
|
||||||
|
import BuiltInImage4 from '@/assets/cover/m_cover_image_4.png';
|
||||||
|
import BuiltInImage5 from '@/assets/cover/m_cover_image_5.png';
|
||||||
|
import BuiltInImage6 from '@/assets/cover/m_cover_image_6.png';
|
||||||
|
|
||||||
export function DocumentHeader({ viewId, doc }: { viewId: string; doc: YDoc }) {
|
export function DocumentHeader({ viewId, doc }: { viewId: string; doc: YDoc }) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@ -16,19 +25,59 @@ export function DocumentHeader({ viewId, doc }: { viewId: string; doc: YDoc }) {
|
|||||||
}
|
}
|
||||||
}, [icon]);
|
}, [icon]);
|
||||||
|
|
||||||
|
const { extra } = usePageInfo(viewId);
|
||||||
|
|
||||||
|
const pageCover = extra.cover;
|
||||||
|
const { cover } = useBlockCover(doc);
|
||||||
|
|
||||||
|
const coverType = useMemo(() => {
|
||||||
|
if (
|
||||||
|
(pageCover && [CoverType.NormalColor, CoverType.GradientColor].includes(pageCover.type)) ||
|
||||||
|
cover?.cover_selection_type === DocCoverType.Color
|
||||||
|
) {
|
||||||
|
return 'color';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CoverType.BuildInImage === pageCover?.type || cover?.cover_selection_type === DocCoverType.Asset) {
|
||||||
|
return 'built_in';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(pageCover && [CoverType.CustomImage, CoverType.UpsplashImage].includes(pageCover.type)) ||
|
||||||
|
cover?.cover_selection_type === DocCoverType.Image
|
||||||
|
) {
|
||||||
|
return 'custom';
|
||||||
|
}
|
||||||
|
}, [cover?.cover_selection_type, pageCover]);
|
||||||
|
|
||||||
|
const coverValue = useMemo(() => {
|
||||||
|
if (coverType === 'built_in') {
|
||||||
|
return {
|
||||||
|
1: BuiltInImage1,
|
||||||
|
2: BuiltInImage2,
|
||||||
|
3: BuiltInImage3,
|
||||||
|
4: BuiltInImage4,
|
||||||
|
5: BuiltInImage5,
|
||||||
|
6: BuiltInImage6,
|
||||||
|
}[pageCover?.value as string];
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageCover?.value || cover?.cover_selection;
|
||||||
|
}, [coverType, cover?.cover_selection, pageCover]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={'document-header mb-[10px] select-none'}>
|
<div ref={ref} className={'document-header mb-[10px] select-none'}>
|
||||||
<div className={'view-banner relative flex w-full flex-col overflow-hidden'}>
|
<div className={'view-banner relative flex w-full flex-col overflow-hidden'}>
|
||||||
<DocumentCover onTextColor={setTextColor} doc={doc} />
|
<DocumentCover onTextColor={setTextColor} coverType={coverType} coverValue={coverValue} />
|
||||||
|
|
||||||
<div className={`relative mx-16 w-[964px] min-w-0 max-w-full overflow-visible max-md:mx-4`}>
|
<div className={`relative mx-16 w-[964px] min-w-0 max-w-full overflow-visible max-md:mx-4`}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: coverValue ? 'absolute' : 'relative',
|
||||||
bottom: '100%',
|
bottom: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
className={'flex items-center gap-2 px-14 pb-10 text-4xl max-md:px-2 max-md:pb-6 max-sm:text-[7vw]'}
|
className={'flex items-center gap-2 px-14 py-8 text-4xl max-md:px-2 max-sm:text-[7vw]'}
|
||||||
>
|
>
|
||||||
<div className={`view-icon`}>{iconObject?.value}</div>
|
<div className={`view-icon`}>{iconObject?.value}</div>
|
||||||
<div className={'flex flex-1 items-center gap-2 overflow-hidden'}>
|
<div className={'flex flex-1 items-center gap-2 overflow-hidden'}>
|
||||||
|
Before Width: | Height: | Size: 275 KiB |
@ -3,7 +3,7 @@ import { Leaf } from '@/components/editor/components/leaf';
|
|||||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { NodeEntry } from 'slate';
|
import { NodeEntry } from 'slate';
|
||||||
import { Editable, ReactEditor } from 'slate-react';
|
import { Editable, ReactEditor, RenderElementProps } from 'slate-react';
|
||||||
import { Element } from './components/element';
|
import { Element } from './components/element';
|
||||||
|
|
||||||
const EditorEditable = ({ editor }: { editor: ReactEditor }) => {
|
const EditorEditable = ({ editor }: { editor: ReactEditor }) => {
|
||||||
@ -17,13 +17,15 @@ const EditorEditable = ({ editor }: { editor: ReactEditor }) => {
|
|||||||
[codeDecorate]
|
[codeDecorate]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderElement = useCallback((props: RenderElementProps) => <Element {...props} />, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Editable
|
<Editable
|
||||||
role={'textbox'}
|
role={'textbox'}
|
||||||
decorate={decorate}
|
decorate={decorate}
|
||||||
className={'px-16 outline-none focus:outline-none max-md:px-4'}
|
className={'px-16 outline-none focus:outline-none max-md:px-4'}
|
||||||
renderLeaf={Leaf}
|
renderLeaf={Leaf}
|
||||||
renderElement={Element}
|
renderElement={renderElement}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoCorrect={'off'}
|
autoCorrect={'off'}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import { YDoc } from '@/application/collab.type';
|
import { YDoc, YFolder, YjsEditorKey } from '@/application/collab.type';
|
||||||
import { DocumentTest } from '@/../cypress/support/document';
|
import { DocumentTest } from '@/../cypress/support/document';
|
||||||
import { applyYDoc } from '@/application/ydoc/apply';
|
import { applyYDoc } from '@/application/ydoc/apply';
|
||||||
|
import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
import { Editor } from './Editor';
|
import { Editor } from './Editor';
|
||||||
import withAppWrapper from '@/components/app/withAppWrapper';
|
import withAppWrapper from '@/components/app/withAppWrapper';
|
||||||
|
|
||||||
describe('<Editor />', () => {
|
describe('<Editor />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.viewport(1280, 720);
|
||||||
|
});
|
||||||
it('renders with a paragraph', () => {
|
it('renders with a paragraph', () => {
|
||||||
const documentTest = new DocumentTest();
|
const documentTest = new DocumentTest();
|
||||||
|
|
||||||
@ -16,21 +20,39 @@ describe('<Editor />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders with a full document', () => {
|
it('renders with a full document', () => {
|
||||||
cy.fixture('full_doc').then((docJson) => {
|
cy.mockDatabase();
|
||||||
|
Object.defineProperty(window.navigator, 'language', { value: 'en-US' });
|
||||||
|
Object.defineProperty(window.navigator, 'languages', { value: ['en-US'] });
|
||||||
|
cy.fixture('folder').then((folderJson) => {
|
||||||
const doc = new Y.Doc();
|
const doc = new Y.Doc();
|
||||||
const state = new Uint8Array(docJson.data.doc_state);
|
const state = new Uint8Array(folderJson.data.doc_state);
|
||||||
|
|
||||||
applyYDoc(doc, state);
|
applyYDoc(doc, state);
|
||||||
renderEditor(doc);
|
|
||||||
|
const folder = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.folder) as YFolder;
|
||||||
|
|
||||||
|
cy.fixture('full_doc').then((docJson) => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const state = new Uint8Array(docJson.data.doc_state);
|
||||||
|
|
||||||
|
applyYDoc(doc, state);
|
||||||
|
renderEditor(doc, folder);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderEditor(doc: YDoc) {
|
function renderEditor(doc: YDoc, folder?: YFolder) {
|
||||||
const AppWrapper = withAppWrapper(() => {
|
const AppWrapper = withAppWrapper(() => {
|
||||||
return (
|
return (
|
||||||
<div className={'h-screen w-screen overflow-y-auto'}>
|
<div className={'h-screen w-screen overflow-y-auto'}>
|
||||||
<Editor doc={doc} readOnly />
|
{folder ? (
|
||||||
|
<FolderProvider folder={folder}>
|
||||||
|
<Editor doc={doc} readOnly />
|
||||||
|
</FolderProvider>
|
||||||
|
) : (
|
||||||
|
<Editor doc={doc} readOnly />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { YDoc } from '@/application/collab.type';
|
import { YDoc } from '@/application/collab.type';
|
||||||
import CollaborativeEditor from '@/components/editor/CollaborativeEditor';
|
import CollaborativeEditor from '@/components/editor/CollaborativeEditor';
|
||||||
import { defaultLayoutStyle, EditorContextProvider, EditorLayoutStyle } from '@/components/editor/EditorContext';
|
import { defaultLayoutStyle, EditorContextProvider, EditorLayoutStyle } from '@/components/editor/EditorContext';
|
||||||
import React from 'react';
|
import React, { memo } from 'react';
|
||||||
import './editor.scss';
|
import './editor.scss';
|
||||||
|
|
||||||
export interface EditorProps {
|
export interface EditorProps {
|
||||||
@ -10,12 +10,12 @@ export interface EditorProps {
|
|||||||
layoutStyle?: EditorLayoutStyle;
|
layoutStyle?: EditorLayoutStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor = ({ readOnly, doc, layoutStyle = defaultLayoutStyle }: EditorProps) => {
|
export const Editor = memo(({ readOnly, doc, layoutStyle = defaultLayoutStyle }: EditorProps) => {
|
||||||
return (
|
return (
|
||||||
<EditorContextProvider layoutStyle={layoutStyle} readOnly={readOnly}>
|
<EditorContextProvider layoutStyle={layoutStyle} readOnly={readOnly}>
|
||||||
<CollaborativeEditor doc={doc} />
|
<CollaborativeEditor doc={doc} />
|
||||||
</EditorContextProvider>
|
</EditorContextProvider>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default Editor;
|
export default Editor;
|
||||||
|
@ -22,5 +22,6 @@ export const CodeBlock = memo(
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})
|
}),
|
||||||
|
(prevProps, nextProps) => JSON.stringify(prevProps.node) === JSON.stringify(nextProps.node)
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg';
|
import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg';
|
||||||
import { useNavigateToView } from '@/application/folder-yjs';
|
import { useNavigateToView } from '@/application/folder-yjs';
|
||||||
import { IdProvider, useId } from '@/components/_shared/context-provider/IdProvider';
|
import { getCurrentWorkspace } from '@/application/services/js-services/storage';
|
||||||
|
import { IdProvider } from '@/components/_shared/context-provider/IdProvider';
|
||||||
import { Database } from '@/components/database';
|
import { Database } from '@/components/database';
|
||||||
|
import { useGetDatabaseId, useLoadDatabase } from '@/components/database/Database.hooks';
|
||||||
|
import { DatabaseContextProvider } from '@/components/database/DatabaseContext';
|
||||||
import { DatabaseNode, EditorElementProps } from '@/components/editor/editor.type';
|
import { DatabaseNode, EditorElementProps } from '@/components/editor/editor.type';
|
||||||
import { Tooltip } from '@mui/material';
|
import { Tooltip } from '@mui/material';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import React, { forwardRef, memo, useCallback, useMemo, useState } from 'react';
|
import React, { forwardRef, memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { BlockType } from '@/application/collab.type';
|
import { BlockType } from '@/application/collab.type';
|
||||||
@ -12,11 +16,10 @@ export const DatabaseBlock = memo(
|
|||||||
forwardRef<HTMLDivElement, EditorElementProps<DatabaseNode>>(({ node, children, ...attributes }, ref) => {
|
forwardRef<HTMLDivElement, EditorElementProps<DatabaseNode>>(({ node, children, ...attributes }, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const viewId = node.data.view_id;
|
const viewId = node.data.view_id;
|
||||||
const workspaceId = useId()?.workspaceId;
|
|
||||||
const type = node.type;
|
const type = node.type;
|
||||||
const navigateToView = useNavigateToView();
|
const navigateToView = useNavigateToView();
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
const [databaseViewId, setDatabaseViewId] = useState<string | undefined>(viewId);
|
||||||
const style = useMemo(() => {
|
const style = useMemo(() => {
|
||||||
const style = {};
|
const style = {};
|
||||||
|
|
||||||
@ -37,13 +40,22 @@ export const DatabaseBlock = memo(
|
|||||||
}, [type]);
|
}, [type]);
|
||||||
|
|
||||||
const handleNavigateToRow = useCallback(
|
const handleNavigateToRow = useCallback(
|
||||||
(viewId: string, rowId: string) => {
|
async (rowId: string) => {
|
||||||
const url = `/view/${workspaceId}/${viewId}?r=${rowId}`;
|
const workspace = await getCurrentWorkspace();
|
||||||
|
|
||||||
|
if (!workspace) return;
|
||||||
|
|
||||||
|
const url = `/view/${workspace.id}/${databaseViewId}?r=${rowId}`;
|
||||||
|
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
},
|
},
|
||||||
[workspaceId]
|
[databaseViewId]
|
||||||
);
|
);
|
||||||
|
const databaseId = useGetDatabaseId(viewId);
|
||||||
|
|
||||||
|
const { doc, rows, notFound } = useLoadDatabase({
|
||||||
|
databaseId,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -57,9 +69,17 @@ export const DatabaseBlock = memo(
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<div contentEditable={false} style={style} className={`container-bg relative flex w-full flex-col px-3`}>
|
<div contentEditable={false} style={style} className={`container-bg relative flex w-full flex-col px-3`}>
|
||||||
{viewId ? (
|
{viewId && doc && rows ? (
|
||||||
<IdProvider workspaceId={workspaceId} objectId={viewId}>
|
<IdProvider objectId={viewId}>
|
||||||
<Database onNavigateToRow={handleNavigateToRow} />
|
<DatabaseContextProvider
|
||||||
|
navigateToRow={handleNavigateToRow}
|
||||||
|
viewId={databaseViewId || viewId}
|
||||||
|
databaseDoc={doc}
|
||||||
|
rowDocMap={rows}
|
||||||
|
readOnly={true}
|
||||||
|
>
|
||||||
|
<Database iidIndex={viewId} viewId={databaseViewId || viewId} onNavigateToView={setDatabaseViewId} />
|
||||||
|
</DatabaseContextProvider>
|
||||||
{isHovering && (
|
{isHovering && (
|
||||||
<div className={'absolute right-4 top-1'}>
|
<div className={'absolute right-4 top-1'}>
|
||||||
<Tooltip placement={'bottom'} title={t('tooltip.openAsPage')}>
|
<Tooltip placement={'bottom'} title={t('tooltip.openAsPage')}>
|
||||||
@ -80,15 +100,22 @@ export const DatabaseBlock = memo(
|
|||||||
<div
|
<div
|
||||||
className={'mt-[10%] flex h-full w-full flex-col items-center gap-2 px-16 text-text-caption max-md:px-4'}
|
className={'mt-[10%] flex h-full w-full flex-col items-center gap-2 px-16 text-text-caption max-md:px-4'}
|
||||||
>
|
>
|
||||||
<div className={'text-sm font-medium'}>{t('document.plugins.database.noDataSource')}</div>
|
{notFound ? (
|
||||||
<div className={'text-xs'}>{t('grid.relation.noDatabaseSelected')}</div>
|
<>
|
||||||
|
<div className={'text-sm font-medium'}>{t('document.plugins.database.noDataSource')}</div>
|
||||||
|
<div className={'text-xs'}>{t('grid.relation.noDatabaseSelected')}</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<CircularProgress />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})
|
}),
|
||||||
|
(prevProps, nextProps) => prevProps.node.data.view_id === nextProps.node.data.view_id
|
||||||
);
|
);
|
||||||
|
|
||||||
export default DatabaseBlock;
|
export default DatabaseBlock;
|
||||||
|
@ -38,7 +38,8 @@ export const MathEquation = memo(
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
(prevProps, nextProps) => JSON.stringify(prevProps.node) === JSON.stringify(nextProps.node)
|
||||||
);
|
);
|
||||||
|
|
||||||
export default MathEquation;
|
export default MathEquation;
|
||||||
|