mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-09-11 11:15:53 +03:00
feat: improve test coverage (#5481)
This commit is contained in:
parent
d73e388d01
commit
3b72f90ca5
16
.github/workflows/web_coverage.yaml
vendored
16
.github/workflows/web_coverage.yaml
vendored
@ -6,6 +6,7 @@ on:
|
|||||||
- ".github/workflows/web2_ci.yaml"
|
- ".github/workflows/web2_ci.yaml"
|
||||||
- "frontend/appflowy_web_app/**"
|
- "frontend/appflowy_web_app/**"
|
||||||
- "frontend/resources/**"
|
- "frontend/resources/**"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
NODE_VERSION: "18.16.0"
|
NODE_VERSION: "18.16.0"
|
||||||
PNPM_VERSION: "8.5.0"
|
PNPM_VERSION: "8.5.0"
|
||||||
@ -52,8 +53,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pnpm run test:unit
|
pnpm run test:unit
|
||||||
|
|
||||||
- name: Generate and post coverage summary
|
- name: Upload coverage to Codecov
|
||||||
working-directory: frontend/appflowy_web_app
|
uses: codecov/codecov-action@v2
|
||||||
run: |
|
with:
|
||||||
pnpm run merge-coverage
|
token: cf9245e0-e136-4e21-b0ee-35755fa0c493
|
||||||
|
files: frontend/appflowy_web_app/coverage/jest/lcov.info,frontend/appflowy_web_app/coverage/cypress/lcov.info
|
||||||
|
flags: appflowy_web_app
|
||||||
|
name: frontend/appflowy_web_app
|
||||||
|
fail_ci_if_error: true
|
||||||
|
verbose: true
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"all": true,
|
"all": true,
|
||||||
"extends": "@istanbuljs/nyc-config-typescript",
|
"extends": "@istanbuljs/nyc-config-babel",
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"src/**/*.tsx"
|
"src/**/*.tsx"
|
||||||
@ -15,7 +15,8 @@
|
|||||||
"text",
|
"text",
|
||||||
"html",
|
"html",
|
||||||
"text-summary",
|
"text-summary",
|
||||||
"json"
|
"json",
|
||||||
|
"lcov"
|
||||||
],
|
],
|
||||||
"temp-dir": "coverage/.nyc_output",
|
"temp-dir": "coverage/.nyc_output",
|
||||||
"report-dir": "coverage/cypress"
|
"report-dir": "coverage/cypress"
|
||||||
|
@ -5,7 +5,7 @@ const esModules = ['lodash-es', 'nanoid'].join('|');
|
|||||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'jsdom',
|
||||||
roots: ['<rootDir>'],
|
roots: ['<rootDir>'],
|
||||||
modulePaths: [compilerOptions.baseUrl],
|
modulePaths: [compilerOptions.baseUrl],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
@ -14,10 +14,28 @@ module.exports = {
|
|||||||
'^nanoid(/(.*)|$)': 'nanoid$1',
|
'^nanoid(/(.*)|$)': 'nanoid$1',
|
||||||
},
|
},
|
||||||
'transform': {
|
'transform': {
|
||||||
|
'^.+\\.(j|t)sx?$': 'ts-jest',
|
||||||
'(.*)/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'],
|
testMatch: ['**/*.test.ts', '**/*.test.tsx'],
|
||||||
coverageDirectory: '<rootDir>/coverage/jest',
|
coverageDirectory: '<rootDir>/coverage/jest',
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
|
coverageProvider: 'v8',
|
||||||
|
coveragePathIgnorePatterns: [
|
||||||
|
'/cypress/',
|
||||||
|
'/coverage/',
|
||||||
|
'/node_modules/',
|
||||||
|
'/__tests__/',
|
||||||
|
'/__mocks__/',
|
||||||
|
'/__fixtures__/',
|
||||||
|
'/__helpers__/',
|
||||||
|
'/__utils__/',
|
||||||
|
'/__constants__/',
|
||||||
|
'/__types__/',
|
||||||
|
'/__mocks__/',
|
||||||
|
'/__stubs__/',
|
||||||
|
'/__fixtures__/',
|
||||||
|
'/application/folder-yjs/',
|
||||||
|
],
|
||||||
};
|
};
|
@ -21,8 +21,7 @@
|
|||||||
"test:components": "cypress run --component --browser chrome --headless",
|
"test:components": "cypress run --component --browser chrome --headless",
|
||||||
"test:unit": "jest --coverage",
|
"test:unit": "jest --coverage",
|
||||||
"test:cy": "cypress run",
|
"test:cy": "cypress run",
|
||||||
"merge-coverage": "node scripts/merge-coverage.cjs",
|
"coverage": "pnpm run test:unit && pnpm run test:components"
|
||||||
"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",
|
||||||
@ -99,11 +98,15 @@
|
|||||||
"yjs": "^13.6.14"
|
"yjs": "^13.6.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/preset-env": "^7.24.7",
|
||||||
|
"@babel/preset-react": "^7.24.7",
|
||||||
|
"@babel/preset-typescript": "^7.24.7",
|
||||||
"@cypress/code-coverage": "^3.12.39",
|
"@cypress/code-coverage": "^3.12.39",
|
||||||
"@istanbuljs/nyc-config-babel": "^3.0.0",
|
"@istanbuljs/nyc-config-babel": "^3.0.0",
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
"@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",
|
||||||
|
"@testing-library/react": "^16.0.0",
|
||||||
"@types/google-protobuf": "^3.15.12",
|
"@types/google-protobuf": "^3.15.12",
|
||||||
"@types/is-hotkey": "^0.1.7",
|
"@types/is-hotkey": "^0.1.7",
|
||||||
"@types/jest": "^29.5.3",
|
"@types/jest": "^29.5.3",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,31 +0,0 @@
|
|||||||
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`);
|
|
||||||
}
|
|
||||||
|
|
@ -369,7 +369,19 @@ export interface YDocument extends Y.Map<unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface YBlocks extends Y.Map<unknown> {
|
export interface YBlocks extends Y.Map<unknown> {
|
||||||
get(key: BlockId): Y.Map<unknown>;
|
get(key: BlockId): YBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YBlock extends Y.Map<unknown> {
|
||||||
|
get(key: YjsEditorKey.block_id | YjsEditorKey.block_parent): BlockId;
|
||||||
|
|
||||||
|
get(key: YjsEditorKey.block_type): BlockType;
|
||||||
|
|
||||||
|
get(key: YjsEditorKey.block_data): string;
|
||||||
|
|
||||||
|
get(key: YjsEditorKey.block_children): ChildrenId;
|
||||||
|
|
||||||
|
get(key: YjsEditorKey.block_external_id): ExternalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface YMeta extends Y.Map<unknown> {
|
export interface YMeta extends Y.Map<unknown> {
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
filterBy,
|
filterBy,
|
||||||
} from '../filter';
|
} from '../filter';
|
||||||
import { expect } from '@jest/globals';
|
import { expect } from '@jest/globals';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
describe('Text filter check', () => {
|
describe('Text filter check', () => {
|
||||||
const text = 'Hello, world!';
|
const text = 'Hello, world!';
|
||||||
@ -540,6 +541,15 @@ describe('Database filterBy', () => {
|
|||||||
expect(result).toBe('1,2,3,4,5,6,7,8,9,10');
|
expect(result).toBe('1,2,3,4,5,6,7,8,9,10');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return all rows for empty rowMap', () => {
|
||||||
|
const { filters, fields } = withTestingData();
|
||||||
|
const rowMap = new Y.Map() as Y.Map<Y.Doc>;
|
||||||
|
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', () => {
|
it('should return rows that match text filter', () => {
|
||||||
const { filters, fields, rowMap } = withTestingData();
|
const { filters, fields, rowMap } = withTestingData();
|
||||||
const filter = withRichTextFilter();
|
const filter = withRichTextFilter();
|
||||||
|
@ -78,5 +78,25 @@
|
|||||||
"field_id": "url_field",
|
"field_id": "url_field",
|
||||||
"condition": "desc",
|
"condition": "desc",
|
||||||
"id": "sort_desc_url_field"
|
"id": "sort_desc_url_field"
|
||||||
|
},
|
||||||
|
"sort_asc_created_at": {
|
||||||
|
"field_id": "created_at_field",
|
||||||
|
"condition": "asc",
|
||||||
|
"id": "sort_asc_created_at"
|
||||||
|
},
|
||||||
|
"sort_desc_created_at": {
|
||||||
|
"field_id": "created_at_field",
|
||||||
|
"condition": "desc",
|
||||||
|
"id": "sort_desc_created_at"
|
||||||
|
},
|
||||||
|
"sort_asc_updated_at": {
|
||||||
|
"field_id": "last_modified_field",
|
||||||
|
"condition": "asc",
|
||||||
|
"id": "sort_asc_updated_at"
|
||||||
|
},
|
||||||
|
"sort_desc_updated_at": {
|
||||||
|
"field_id": "last_modified_field",
|
||||||
|
"condition": "desc",
|
||||||
|
"id": "sort_desc_updated_at"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,17 @@
|
|||||||
import { Row } from '@/application/database-yjs';
|
import { FieldType, Row } from '@/application/database-yjs';
|
||||||
import { withTestingData } from '@/application/database-yjs/__tests__/withTestingData';
|
import { withTestingData } from '@/application/database-yjs/__tests__/withTestingData';
|
||||||
import { withTestingRows } from '@/application/database-yjs/__tests__/withTestingRows';
|
import { withTestingRows } from '@/application/database-yjs/__tests__/withTestingRows';
|
||||||
import { expect } from '@jest/globals';
|
import { expect } from '@jest/globals';
|
||||||
import { groupByField } from '../group';
|
import { groupByField } from '../group';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import {
|
||||||
|
YDatabaseField,
|
||||||
|
YDatabaseFieldTypeOption,
|
||||||
|
YjsDatabaseKey,
|
||||||
|
YjsEditorKey,
|
||||||
|
YMapFieldTypeOption,
|
||||||
|
} from '@/application/collab.type';
|
||||||
|
import { YjsEditor } from '@/application/slate-yjs';
|
||||||
|
|
||||||
describe('Database group', () => {
|
describe('Database group', () => {
|
||||||
let rows: Row[];
|
let rows: Row[];
|
||||||
@ -95,4 +104,69 @@ describe('Database group', () => {
|
|||||||
]);
|
]);
|
||||||
expect(result).toEqual(expectRes);
|
expect(result).toEqual(expectRes);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not group if no options', () => {
|
||||||
|
const { fields, rowMap } = withTestingData();
|
||||||
|
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, 'another_single_select_field');
|
||||||
|
field.set(YjsDatabaseKey.type, String(FieldType.SingleSelect));
|
||||||
|
field.set(YjsDatabaseKey.last_modified, now.valueOf());
|
||||||
|
field.set(YjsDatabaseKey.type_option, typeOption);
|
||||||
|
fields.set('another_single_select_field', field);
|
||||||
|
expect(groupByField(rows, rowMap, field)).toBeUndefined();
|
||||||
|
|
||||||
|
const selectTypeOption = new Y.Map() as YMapFieldTypeOption;
|
||||||
|
|
||||||
|
typeOption.set(String(FieldType.SingleSelect), selectTypeOption);
|
||||||
|
selectTypeOption.set(YjsDatabaseKey.content, JSON.stringify({ disable_color: false, options: [] }));
|
||||||
|
const expectRes = new Map([['another_single_select_field', rows]]);
|
||||||
|
expect(groupByField(rows, rowMap, field)).toEqual(expectRes);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty selected ids', () => {
|
||||||
|
const { fields, rowMap } = withTestingData();
|
||||||
|
const cell = rowMap
|
||||||
|
.get('1')
|
||||||
|
?.getMap(YjsEditorKey.data_section)
|
||||||
|
?.get(YjsEditorKey.database_row)
|
||||||
|
?.get(YjsDatabaseKey.cells)
|
||||||
|
?.get('single_select_field');
|
||||||
|
cell?.set(YjsDatabaseKey.data, null);
|
||||||
|
|
||||||
|
const field = fields.get('single_select_field');
|
||||||
|
const result = groupByField(rows, rowMap, field);
|
||||||
|
expect(result).toEqual(
|
||||||
|
new Map([
|
||||||
|
['single_select_field', [{ id: '1', 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 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
[
|
||||||
|
{ id: '4', height: 37 },
|
||||||
|
{ id: '7', height: 37 },
|
||||||
|
{ id: '10', height: 37 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
import { withTestingCheckboxCell, withTestingDateCell } from '@/application/database-yjs/__tests__/withTestingCell';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import {
|
||||||
|
FieldType,
|
||||||
|
parseSelectOptionTypeOptions,
|
||||||
|
parseRelationTypeOption,
|
||||||
|
parseNumberTypeOptions,
|
||||||
|
} from '@/application/database-yjs';
|
||||||
|
import { YDatabaseField, YDatabaseFieldTypeOption, YjsDatabaseKey } from '@/application/collab.type';
|
||||||
|
import { withNumberTestingField, withRelationTestingField } from '@/application/database-yjs/__tests__/withTestingField';
|
||||||
|
|
||||||
|
describe('parseYDatabaseCellToCell', () => {
|
||||||
|
it('should parse a DateTime cell', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const cell = withTestingDateCell();
|
||||||
|
doc.getMap('cells').set('date_field', cell);
|
||||||
|
const parsedCell = parseYDatabaseCellToCell(cell);
|
||||||
|
expect(parsedCell.data).not.toBe(undefined);
|
||||||
|
expect(parsedCell.createdAt).not.toBe(undefined);
|
||||||
|
expect(parsedCell.lastModified).not.toBe(undefined);
|
||||||
|
expect(parsedCell.fieldType).toBe(Number(FieldType.DateTime));
|
||||||
|
});
|
||||||
|
it('should parse a Checkbox cell', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const cell = withTestingCheckboxCell();
|
||||||
|
doc.getMap('cells').set('checkbox_field', cell);
|
||||||
|
const parsedCell = parseYDatabaseCellToCell(cell);
|
||||||
|
expect(parsedCell.data).toBe(true);
|
||||||
|
expect(parsedCell.createdAt).not.toBe(undefined);
|
||||||
|
expect(parsedCell.lastModified).not.toBe(undefined);
|
||||||
|
expect(parsedCell.fieldType).toBe(Number(FieldType.Checkbox));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Select option field parse', () => {
|
||||||
|
it('should parse select option type options', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
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, 'single_select_field');
|
||||||
|
field.set(YjsDatabaseKey.type, String(FieldType.SingleSelect));
|
||||||
|
field.set(YjsDatabaseKey.last_modified, now.valueOf());
|
||||||
|
field.set(YjsDatabaseKey.type_option, typeOption);
|
||||||
|
doc.getMap('fields').set('single_select_field', field);
|
||||||
|
expect(parseSelectOptionTypeOptions(field)).toEqual(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('number field parse', () => {
|
||||||
|
it('should parse number field', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const field = withNumberTestingField();
|
||||||
|
doc.getMap('fields').set('number_field', field);
|
||||||
|
expect(parseNumberTypeOptions(field)).toEqual({
|
||||||
|
format: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('relation field parse', () => {
|
||||||
|
it('should parse relation field', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const field = withRelationTestingField();
|
||||||
|
doc.getMap('fields').set('relation_field', field);
|
||||||
|
expect(parseRelationTypeOption(field)).toEqual(undefined);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,283 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import {
|
||||||
|
useCalendarEventsSelector,
|
||||||
|
useCellSelector,
|
||||||
|
useFieldSelector,
|
||||||
|
useFieldsSelector,
|
||||||
|
useFilterSelector,
|
||||||
|
useFiltersSelector,
|
||||||
|
useGroup,
|
||||||
|
useGroupsSelector,
|
||||||
|
usePrimaryFieldId,
|
||||||
|
useRowDataSelector,
|
||||||
|
useRowDocMapSelector,
|
||||||
|
useRowMetaSelector,
|
||||||
|
useRowOrdersSelector,
|
||||||
|
useRowsByGroup,
|
||||||
|
useSortSelector,
|
||||||
|
useSortsSelector,
|
||||||
|
} from '../selector';
|
||||||
|
import { useDatabaseViewId } from '../context';
|
||||||
|
import { IdProvider } from '@/components/_shared/context-provider/IdProvider';
|
||||||
|
import { DatabaseContextProvider } from '@/components/database/DatabaseContext';
|
||||||
|
import { withTestingDatabase } from '@/application/database-yjs/__tests__/withTestingData';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
import { YDoc, YjsDatabaseKey, YjsEditorKey, YSharedRoot } from '@/application/collab.type';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
import { withNumberTestingField, withTestingFields } from '@/application/database-yjs/__tests__/withTestingField';
|
||||||
|
import { withTestingRows } from '@/application/database-yjs/__tests__/withTestingRows';
|
||||||
|
|
||||||
|
const wrapperCreator =
|
||||||
|
(viewId: string, doc: YDoc, rowDocMap: Y.Map<YDoc>) =>
|
||||||
|
({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<IdProvider objectId={viewId}>
|
||||||
|
<DatabaseContextProvider viewId={viewId} databaseDoc={doc} rowDocMap={rowDocMap} readOnly={true}>
|
||||||
|
{children}
|
||||||
|
</DatabaseContextProvider>
|
||||||
|
</IdProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Database selector', () => {
|
||||||
|
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
|
||||||
|
let rowDocMap: Y.Map<YDoc>;
|
||||||
|
let doc: YDoc;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const data = withTestingDatabase('1');
|
||||||
|
|
||||||
|
doc = data.doc;
|
||||||
|
rowDocMap = data.rowDocMap;
|
||||||
|
wrapper = wrapperCreator('1', doc, rowDocMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select a field', () => {
|
||||||
|
const { result } = renderHook(() => useFieldSelector('number_field'), { wrapper });
|
||||||
|
|
||||||
|
const tempDoc = new Y.Doc();
|
||||||
|
const field = withNumberTestingField();
|
||||||
|
|
||||||
|
tempDoc.getMap().set('number_field', field);
|
||||||
|
|
||||||
|
expect(result.current.field?.toJSON()).toEqual(field.toJSON());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select all fields', () => {
|
||||||
|
const { result } = renderHook(() => useFieldsSelector(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current.map((item) => item.fieldId)).toEqual(Array.from(withTestingFields().keys()));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select all filters', () => {
|
||||||
|
const { result } = renderHook(() => useFiltersSelector(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current).toEqual(['filter_multi_select_field']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select a filter', () => {
|
||||||
|
const { result } = renderHook(() => useFilterSelector('filter_multi_select_field'), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
content: '1,3',
|
||||||
|
condition: 2,
|
||||||
|
fieldId: 'multi_select_field',
|
||||||
|
id: 'filter_multi_select_field',
|
||||||
|
filterType: NaN,
|
||||||
|
optionIds: ['1', '3'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select all sorts', () => {
|
||||||
|
const { result } = renderHook(() => useSortsSelector(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current).toEqual(['sort_asc_text_field']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select a sort', () => {
|
||||||
|
const { result } = renderHook(() => useSortSelector('sort_asc_text_field'), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
fieldId: 'text_field',
|
||||||
|
id: 'sort_asc_text_field',
|
||||||
|
condition: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select all groups', () => {
|
||||||
|
const { result } = renderHook(() => useGroupsSelector(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current).toEqual(['g:single_select_field']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select a group', () => {
|
||||||
|
const { result } = renderHook(() => useGroup('g:single_select_field'), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
fieldId: 'single_select_field',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'single_select_field',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select rows by group', () => {
|
||||||
|
const { result } = renderHook(() => useRowsByGroup('g:single_select_field'), { wrapper });
|
||||||
|
|
||||||
|
const { fieldId, columns, notFound, groupResult } = result.current;
|
||||||
|
|
||||||
|
expect(fieldId).toEqual('single_select_field');
|
||||||
|
expect(columns).toEqual([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'single_select_field',
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(notFound).toBeFalsy();
|
||||||
|
|
||||||
|
expect(groupResult).toEqual(
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
'1',
|
||||||
|
[
|
||||||
|
{ id: '1', height: 37 },
|
||||||
|
{ id: '7', height: 37 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'2',
|
||||||
|
[
|
||||||
|
{ id: '2', height: 37 },
|
||||||
|
{ id: '8', height: 37 },
|
||||||
|
{ id: '5', height: 37 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'3',
|
||||||
|
[
|
||||||
|
{ id: '9', height: 37 },
|
||||||
|
{ id: '3', height: 37 },
|
||||||
|
{ id: '6', height: 37 },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select all row orders', () => {
|
||||||
|
const { result } = renderHook(() => useRowOrdersSelector(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current?.map((item) => item.id).join(',')).toEqual('9,2,3,1,6,8,5,7');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select all row doc map', () => {
|
||||||
|
const { result } = renderHook(() => useRowDocMapSelector(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current.rows).toEqual(rowDocMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select a row data', () => {
|
||||||
|
const rows = withTestingRows();
|
||||||
|
const { result } = renderHook(() => useRowDataSelector(rows[0].id), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current.row.toJSON()).toEqual(
|
||||||
|
rowDocMap.get(rows[0].id)?.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database_row)?.toJSON()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select a cell', () => {
|
||||||
|
const rows = withTestingRows();
|
||||||
|
const { result } = renderHook(
|
||||||
|
() =>
|
||||||
|
useCellSelector({
|
||||||
|
rowId: rows[0].id,
|
||||||
|
fieldId: 'number_field',
|
||||||
|
}),
|
||||||
|
{ wrapper }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
createdAt: NaN,
|
||||||
|
data: 123,
|
||||||
|
fieldType: 1,
|
||||||
|
lastModified: NaN,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select a primary field id', () => {
|
||||||
|
const { result } = renderHook(() => usePrimaryFieldId(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current).toEqual('text_field');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select a row meta', () => {
|
||||||
|
const rows = withTestingRows();
|
||||||
|
const { result } = renderHook(() => useRowMetaSelector(rows[0].id), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current?.documentId).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select all calendar events', () => {
|
||||||
|
const { result } = renderHook(() => useCalendarEventsSelector(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current.events.length).toEqual(8);
|
||||||
|
expect(result.current.emptyEvents.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select view id', () => {
|
||||||
|
const { result } = renderHook(() => useDatabaseViewId(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current).toEqual('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select all rows if filter is not found', () => {
|
||||||
|
const view = (doc.get(YjsEditorKey.data_section) as YSharedRoot)
|
||||||
|
.get(YjsEditorKey.database)
|
||||||
|
.get(YjsDatabaseKey.views)
|
||||||
|
.get('1');
|
||||||
|
|
||||||
|
view.set(YjsDatabaseKey.filters, new Y.Array());
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useRowOrdersSelector(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current?.map((item) => item.id).join(',')).toEqual('9,2,3,4,1,6,10,8,5,7');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select original row orders if sorts is not found', () => {
|
||||||
|
const view = (doc.get(YjsEditorKey.data_section) as YSharedRoot)
|
||||||
|
.get(YjsEditorKey.database)
|
||||||
|
.get(YjsDatabaseKey.views)
|
||||||
|
.get('1');
|
||||||
|
|
||||||
|
view.set(YjsDatabaseKey.sorts, new Y.Array());
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useRowOrdersSelector(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current?.map((item) => item.id).join(',')).toEqual('1,2,3,5,6,7,8,9');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select all rows if filters and sorts are not found', () => {
|
||||||
|
const view = (doc.get(YjsEditorKey.data_section) as YSharedRoot)
|
||||||
|
.get(YjsEditorKey.database)
|
||||||
|
.get(YjsDatabaseKey.views)
|
||||||
|
.get('1');
|
||||||
|
|
||||||
|
view.set(YjsDatabaseKey.filters, new Y.Array());
|
||||||
|
view.set(YjsDatabaseKey.sorts, new Y.Array());
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useRowOrdersSelector(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current?.map((item) => item.id).join(',')).toEqual('1,2,3,4,5,6,7,8,9,10');
|
||||||
|
});
|
||||||
|
});
|
@ -4,7 +4,9 @@ import { withTestingRows } from '@/application/database-yjs/__tests__/withTestin
|
|||||||
import {
|
import {
|
||||||
withCheckboxSort,
|
withCheckboxSort,
|
||||||
withChecklistSort,
|
withChecklistSort,
|
||||||
|
withCreatedAtSort,
|
||||||
withDateTimeSort,
|
withDateTimeSort,
|
||||||
|
withLastModifiedSort,
|
||||||
withMultiSelectOptionSort,
|
withMultiSelectOptionSort,
|
||||||
withNumberSort,
|
withNumberSort,
|
||||||
withRichTextSort,
|
withRichTextSort,
|
||||||
@ -19,10 +21,12 @@ import {
|
|||||||
withSelectOptionTestingField,
|
withSelectOptionTestingField,
|
||||||
withURLTestingField,
|
withURLTestingField,
|
||||||
withChecklistTestingField,
|
withChecklistTestingField,
|
||||||
|
withRelationTestingField,
|
||||||
} from './withTestingField';
|
} from './withTestingField';
|
||||||
import { sortBy, parseCellDataForSort } from '../sort';
|
import { sortBy, parseCellDataForSort } from '../sort';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
import { expect } from '@jest/globals';
|
import { expect } from '@jest/globals';
|
||||||
|
import { YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||||
|
|
||||||
describe('parseCellDataForSort', () => {
|
describe('parseCellDataForSort', () => {
|
||||||
it('should parse data correctly based on field type', () => {
|
it('should parse data correctly based on field type', () => {
|
||||||
@ -127,6 +131,17 @@ describe('parseCellDataForSort', () => {
|
|||||||
|
|
||||||
expect(result).toBe(0);
|
expect(result).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return empty string for Relation field', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const field = withRelationTestingField();
|
||||||
|
doc.getMap().set('field', field);
|
||||||
|
const data = '';
|
||||||
|
|
||||||
|
const result = parseCellDataForSort(field, data);
|
||||||
|
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Database sortBy', () => {
|
describe('Database sortBy', () => {
|
||||||
@ -136,6 +151,53 @@ describe('Database sortBy', () => {
|
|||||||
rows = withTestingRows();
|
rows = withTestingRows();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not sort rows if no sort is provided', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
|
||||||
|
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 not sort rows if no rows are provided', () => {
|
||||||
|
const { sorts, fields } = withTestingData();
|
||||||
|
const rowMap = new Y.Map() as Y.Map<Y.Doc>;
|
||||||
|
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 return default data if rowMeta is not found', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withNumberSort();
|
||||||
|
sorts.push([sort]);
|
||||||
|
rowMap.delete('1');
|
||||||
|
|
||||||
|
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 return default data if cell is not found', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withNumberSort();
|
||||||
|
sorts.push([sort]);
|
||||||
|
const rowDoc = rowMap.get('1');
|
||||||
|
rowDoc
|
||||||
|
?.getMap(YjsEditorKey.data_section)
|
||||||
|
.get(YjsEditorKey.database_row)
|
||||||
|
?.get(YjsDatabaseKey.cells)
|
||||||
|
.delete('number_field');
|
||||||
|
|
||||||
|
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 ascending order', () => {
|
it('should sort by number field in ascending order', () => {
|
||||||
const { sorts, fields, rowMap } = withTestingData();
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
const sort = withNumberSort();
|
const sort = withNumberSort();
|
||||||
@ -311,4 +373,25 @@ describe('Database sortBy', () => {
|
|||||||
.join(',');
|
.join(',');
|
||||||
expect(sortedRows).toBe('3,9,1,2,5,6,7,8,4,10');
|
expect(sortedRows).toBe('3,9,1,2,5,6,7,8,4,10');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should sort by CreatedAt field in ascending order', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withCreatedAtSort();
|
||||||
|
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 LastEditedTime field', () => {
|
||||||
|
const { sorts, fields, rowMap } = withTestingData();
|
||||||
|
const sort = withLastModifiedSort();
|
||||||
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
import * as Y from 'yjs';
|
||||||
|
import { YDatabaseCell, YjsDatabaseKey } from '@/application/collab.type';
|
||||||
|
import { FieldType } from '@/application/database-yjs';
|
||||||
|
|
||||||
|
export function withTestingDateCell() {
|
||||||
|
const cell = new Y.Map() as YDatabaseCell;
|
||||||
|
|
||||||
|
cell.set(YjsDatabaseKey.id, 'date_field');
|
||||||
|
cell.set(YjsDatabaseKey.data, Date.now());
|
||||||
|
cell.set(YjsDatabaseKey.field_type, Number(FieldType.DateTime));
|
||||||
|
cell.set(YjsDatabaseKey.created_at, Date.now());
|
||||||
|
cell.set(YjsDatabaseKey.last_modified, Date.now());
|
||||||
|
cell.set(YjsDatabaseKey.end_timestamp, Date.now() + 1000);
|
||||||
|
cell.set(YjsDatabaseKey.include_time, true);
|
||||||
|
cell.set(YjsDatabaseKey.is_range, true);
|
||||||
|
cell.set(YjsDatabaseKey.reminder_id, 'reminderId');
|
||||||
|
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTestingCheckboxCell() {
|
||||||
|
const cell = new Y.Map() as YDatabaseCell;
|
||||||
|
|
||||||
|
cell.set(YjsDatabaseKey.id, 'checkbox_field');
|
||||||
|
cell.set(YjsDatabaseKey.data, 'Yes');
|
||||||
|
cell.set(YjsDatabaseKey.field_type, Number(FieldType.Checkbox));
|
||||||
|
cell.set(YjsDatabaseKey.created_at, Date.now());
|
||||||
|
cell.set(YjsDatabaseKey.last_modified, Date.now());
|
||||||
|
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTestingSingleOptionCell() {
|
||||||
|
const cell = new Y.Map() as YDatabaseCell;
|
||||||
|
|
||||||
|
cell.set(YjsDatabaseKey.id, 'single_select_field');
|
||||||
|
cell.set(YjsDatabaseKey.data, 'optionId');
|
||||||
|
cell.set(YjsDatabaseKey.field_type, Number(FieldType.SingleSelect));
|
||||||
|
cell.set(YjsDatabaseKey.created_at, Date.now());
|
||||||
|
cell.set(YjsDatabaseKey.last_modified, Date.now());
|
||||||
|
|
||||||
|
return cell;
|
||||||
|
}
|
@ -1,7 +1,29 @@
|
|||||||
import { YDatabaseFields, YDatabaseFilters, YDatabaseSorts } from '@/application/collab.type';
|
import {
|
||||||
|
YDatabase,
|
||||||
|
YDatabaseField,
|
||||||
|
YDatabaseFields,
|
||||||
|
YDatabaseFilters,
|
||||||
|
YDatabaseGroup,
|
||||||
|
YDatabaseGroupColumn,
|
||||||
|
YDatabaseGroupColumns,
|
||||||
|
YDatabaseLayoutSettings,
|
||||||
|
YDatabaseSorts,
|
||||||
|
YDatabaseView,
|
||||||
|
YDatabaseViews,
|
||||||
|
YDoc,
|
||||||
|
YjsDatabaseKey,
|
||||||
|
YjsEditorKey,
|
||||||
|
} from '@/application/collab.type';
|
||||||
import { withTestingFields } from '@/application/database-yjs/__tests__/withTestingField';
|
import { withTestingFields } from '@/application/database-yjs/__tests__/withTestingField';
|
||||||
import { withTestingRowDataMap } from '@/application/database-yjs/__tests__/withTestingRows';
|
import {
|
||||||
|
withTestingRowData,
|
||||||
|
withTestingRowDataMap,
|
||||||
|
withTestingRows,
|
||||||
|
} from '@/application/database-yjs/__tests__/withTestingRows';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
import { withMultiSelectOptionFilter } from '@/application/database-yjs/__tests__/withTestingFilters';
|
||||||
|
import { withRichTextSort } from '@/application/database-yjs/__tests__/withTestingSorts';
|
||||||
|
import { metaIdFromRowId, RowMetaKey } from '@/application/database-yjs';
|
||||||
|
|
||||||
export function withTestingData() {
|
export function withTestingData() {
|
||||||
const doc = new Y.Doc();
|
const doc = new Y.Doc();
|
||||||
@ -27,5 +49,133 @@ export function withTestingData() {
|
|||||||
rowMap,
|
rowMap,
|
||||||
sorts,
|
sorts,
|
||||||
filters,
|
filters,
|
||||||
|
doc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTestingDatabase(viewId: string) {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
const sharedRoot = doc.getMap(YjsEditorKey.data_section);
|
||||||
|
const database = new Y.Map() as YDatabase;
|
||||||
|
|
||||||
|
sharedRoot.set(YjsEditorKey.database, database);
|
||||||
|
|
||||||
|
const fields = withTestingFields() as YDatabaseFields;
|
||||||
|
|
||||||
|
database.set(YjsDatabaseKey.fields, fields);
|
||||||
|
database.set(YjsDatabaseKey.id, viewId);
|
||||||
|
|
||||||
|
const metas = new Y.Map();
|
||||||
|
|
||||||
|
database.set(YjsDatabaseKey.metas, metas);
|
||||||
|
metas.set(YjsDatabaseKey.iid, viewId);
|
||||||
|
|
||||||
|
const views = new Y.Map() as YDatabaseViews;
|
||||||
|
|
||||||
|
database.set(YjsDatabaseKey.views, views);
|
||||||
|
|
||||||
|
const view = new Y.Map() as YDatabaseView;
|
||||||
|
|
||||||
|
views.set('1', view);
|
||||||
|
view.set(YjsDatabaseKey.id, viewId);
|
||||||
|
view.set(YjsDatabaseKey.layout, 0);
|
||||||
|
view.set(YjsDatabaseKey.name, 'View 1');
|
||||||
|
view.set(YjsDatabaseKey.database_id, viewId);
|
||||||
|
|
||||||
|
const layoutSetting = new Y.Map() as YDatabaseLayoutSettings;
|
||||||
|
|
||||||
|
const calendarSetting = new Y.Map();
|
||||||
|
|
||||||
|
calendarSetting.set(YjsDatabaseKey.field_id, 'date_field');
|
||||||
|
layoutSetting.set('2', calendarSetting);
|
||||||
|
|
||||||
|
view.set(YjsDatabaseKey.layout_settings, layoutSetting);
|
||||||
|
|
||||||
|
const filters = new Y.Array() as YDatabaseFilters;
|
||||||
|
const filter = withMultiSelectOptionFilter();
|
||||||
|
|
||||||
|
filters.push([filter]);
|
||||||
|
|
||||||
|
const sorts = new Y.Array() as YDatabaseSorts;
|
||||||
|
const sort = withRichTextSort();
|
||||||
|
|
||||||
|
sorts.push([sort]);
|
||||||
|
|
||||||
|
const groups = new Y.Array();
|
||||||
|
const group = new Y.Map() as YDatabaseGroup;
|
||||||
|
|
||||||
|
groups.push([group]);
|
||||||
|
group.set(YjsDatabaseKey.id, 'g:single_select_field');
|
||||||
|
group.set(YjsDatabaseKey.field_id, 'single_select_field');
|
||||||
|
group.set(YjsDatabaseKey.type, '3');
|
||||||
|
group.set(YjsDatabaseKey.content, '');
|
||||||
|
|
||||||
|
const groupColumns = new Y.Array() as YDatabaseGroupColumns;
|
||||||
|
|
||||||
|
group.set(YjsDatabaseKey.groups, groupColumns);
|
||||||
|
|
||||||
|
const column1 = new Y.Map() as YDatabaseGroupColumn;
|
||||||
|
const column2 = new Y.Map() as YDatabaseGroupColumn;
|
||||||
|
|
||||||
|
column1.set(YjsDatabaseKey.id, '1');
|
||||||
|
column1.set(YjsDatabaseKey.visible, true);
|
||||||
|
column2.set(YjsDatabaseKey.id, 'single_select_field');
|
||||||
|
column2.set(YjsDatabaseKey.visible, true);
|
||||||
|
|
||||||
|
groupColumns.push([column1]);
|
||||||
|
groupColumns.push([column2]);
|
||||||
|
|
||||||
|
view.set(YjsDatabaseKey.filters, filters);
|
||||||
|
view.set(YjsDatabaseKey.sorts, sorts);
|
||||||
|
view.set(YjsDatabaseKey.groups, groups);
|
||||||
|
|
||||||
|
const fieldSettings = new Y.Map();
|
||||||
|
const fieldOrder = new Y.Array();
|
||||||
|
const rowOrders = new Y.Array();
|
||||||
|
|
||||||
|
Array.from(fields).forEach(([fieldId, field]) => {
|
||||||
|
const setting = new Y.Map();
|
||||||
|
|
||||||
|
if (fieldId === 'text_field') {
|
||||||
|
(field as YDatabaseField).set(YjsDatabaseKey.is_primary, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldOrder.push([fieldId]);
|
||||||
|
fieldSettings.set(fieldId, setting);
|
||||||
|
setting.set(YjsDatabaseKey.visibility, 0);
|
||||||
|
});
|
||||||
|
const rows = withTestingRows();
|
||||||
|
|
||||||
|
rows.forEach(({ id, height }) => {
|
||||||
|
const row = new Y.Map();
|
||||||
|
|
||||||
|
row.set(YjsDatabaseKey.id, id);
|
||||||
|
row.set(YjsDatabaseKey.height, height);
|
||||||
|
rowOrders.push([row]);
|
||||||
|
});
|
||||||
|
|
||||||
|
view.set(YjsDatabaseKey.field_settings, fieldSettings);
|
||||||
|
view.set(YjsDatabaseKey.field_orders, fieldOrder);
|
||||||
|
view.set(YjsDatabaseKey.row_orders, rowOrders);
|
||||||
|
|
||||||
|
const rowMapDoc = new Y.Doc();
|
||||||
|
|
||||||
|
const rowMapFolder = rowMapDoc.getMap();
|
||||||
|
|
||||||
|
rows.forEach((row, index) => {
|
||||||
|
const rowDoc = new Y.Doc();
|
||||||
|
const rowData = withTestingRowData(row.id, index);
|
||||||
|
const rowMeta = new Y.Map();
|
||||||
|
const parser = metaIdFromRowId('281e76fb-712e-59e2-8370-678bf0788355');
|
||||||
|
|
||||||
|
rowMeta.set(parser(RowMetaKey.IconId), '😊');
|
||||||
|
rowDoc.getMap(YjsEditorKey.data_section).set(YjsEditorKey.meta, rowMeta);
|
||||||
|
rowDoc.getMap(YjsEditorKey.data_section).set(YjsEditorKey.database_row, rowData);
|
||||||
|
rowMapFolder.set(row.id, rowDoc);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
rowDocMap: rowMapFolder as Y.Map<YDoc>,
|
||||||
|
doc: doc as YDoc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,8 @@ import {
|
|||||||
YjsDatabaseKey,
|
YjsDatabaseKey,
|
||||||
YMapFieldTypeOption,
|
YMapFieldTypeOption,
|
||||||
} from '@/application/collab.type';
|
} from '@/application/collab.type';
|
||||||
import { FieldType, SelectOptionColor } from '@/application/database-yjs';
|
import { FieldType } from '@/application/database-yjs';
|
||||||
|
import { SelectOptionColor } from '@/application/database-yjs/fields/select-option';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
export function withTestingFields() {
|
export function withTestingFields() {
|
||||||
@ -39,6 +40,14 @@ export function withTestingFields() {
|
|||||||
|
|
||||||
fields.set('checklist_field', checklistField);
|
fields.set('checklist_field', checklistField);
|
||||||
|
|
||||||
|
const createdAtField = withCreatedAtTestingField();
|
||||||
|
|
||||||
|
fields.set('created_at_field', createdAtField);
|
||||||
|
|
||||||
|
const lastModifiedField = withLastModifiedTestingField();
|
||||||
|
|
||||||
|
fields.set('last_modified_field', lastModifiedField);
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,13 +65,31 @@ export function withRichTextTestingField() {
|
|||||||
|
|
||||||
export function withNumberTestingField() {
|
export function withNumberTestingField() {
|
||||||
const field = new Y.Map() as YDatabaseField;
|
const field = new Y.Map() as YDatabaseField;
|
||||||
|
|
||||||
const now = Date.now().toString();
|
|
||||||
|
|
||||||
field.set(YjsDatabaseKey.name, 'Number Field');
|
field.set(YjsDatabaseKey.name, 'Number Field');
|
||||||
field.set(YjsDatabaseKey.id, 'number_field');
|
field.set(YjsDatabaseKey.id, 'number_field');
|
||||||
field.set(YjsDatabaseKey.type, String(FieldType.Number));
|
field.set(YjsDatabaseKey.type, String(FieldType.Number));
|
||||||
|
const typeOption = new Y.Map() as YDatabaseFieldTypeOption;
|
||||||
|
|
||||||
|
const numberTypeOption = new Y.Map() as YMapFieldTypeOption;
|
||||||
|
|
||||||
|
typeOption.set(String(FieldType.Number), numberTypeOption);
|
||||||
|
numberTypeOption.set(YjsDatabaseKey.format, '0');
|
||||||
|
field.set(YjsDatabaseKey.type_option, typeOption);
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withRelationTestingField() {
|
||||||
|
const field = new Y.Map() as YDatabaseField;
|
||||||
|
const typeOption = new Y.Map() as YDatabaseFieldTypeOption;
|
||||||
|
const now = Date.now().toString();
|
||||||
|
|
||||||
|
field.set(YjsDatabaseKey.name, 'Relation Field');
|
||||||
|
field.set(YjsDatabaseKey.id, 'relation_field');
|
||||||
|
field.set(YjsDatabaseKey.type, String(FieldType.Relation));
|
||||||
field.set(YjsDatabaseKey.last_modified, now.valueOf());
|
field.set(YjsDatabaseKey.last_modified, now.valueOf());
|
||||||
|
field.set(YjsDatabaseKey.type_option, typeOption);
|
||||||
|
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
@ -151,3 +178,27 @@ export function withChecklistTestingField() {
|
|||||||
|
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function withCreatedAtTestingField() {
|
||||||
|
const field = new Y.Map() as YDatabaseField;
|
||||||
|
const now = Date.now().toString();
|
||||||
|
|
||||||
|
field.set(YjsDatabaseKey.name, 'Created At Field');
|
||||||
|
field.set(YjsDatabaseKey.id, 'created_at_field');
|
||||||
|
field.set(YjsDatabaseKey.type, String(FieldType.CreatedTime));
|
||||||
|
field.set(YjsDatabaseKey.last_modified, now.valueOf());
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withLastModifiedTestingField() {
|
||||||
|
const field = new Y.Map() as YDatabaseField;
|
||||||
|
const now = Date.now().toString();
|
||||||
|
|
||||||
|
field.set(YjsDatabaseKey.name, 'Last Modified Field');
|
||||||
|
field.set(YjsDatabaseKey.id, 'last_modified_field');
|
||||||
|
field.set(YjsDatabaseKey.type, String(FieldType.LastEditedTime));
|
||||||
|
field.set(YjsDatabaseKey.last_modified, now.valueOf());
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
@ -39,6 +39,8 @@ export function withTestingRowData(id: string, index: number) {
|
|||||||
|
|
||||||
rowData.set(YjsDatabaseKey.id, id);
|
rowData.set(YjsDatabaseKey.id, id);
|
||||||
rowData.set(YjsDatabaseKey.height, 37);
|
rowData.set(YjsDatabaseKey.height, 37);
|
||||||
|
rowData.set(YjsDatabaseKey.last_modified, Date.now() + index * 1000);
|
||||||
|
rowData.set(YjsDatabaseKey.created_at, Date.now() + index * 1000);
|
||||||
|
|
||||||
const cells = new Y.Map() as YDatabaseCells;
|
const cells = new Y.Map() as YDatabaseCells;
|
||||||
|
|
||||||
|
@ -89,3 +89,25 @@ export function withChecklistSort(isAscending: boolean = true) {
|
|||||||
|
|
||||||
return sort;
|
return sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function withCreatedAtSort(isAscending: boolean = true) {
|
||||||
|
const sort = new Y.Map() as YDatabaseSort;
|
||||||
|
const sortJSON = isAscending ? sortsJson.sort_asc_created_at : sortsJson.sort_desc_created_at;
|
||||||
|
|
||||||
|
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 withLastModifiedSort(isAscending: boolean = true) {
|
||||||
|
const sort = new Y.Map() as YDatabaseSort;
|
||||||
|
const sortJSON = isAscending ? sortsJson.sort_asc_updated_at : sortsJson.sort_desc_updated_at;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FieldId, RowId } from '@/application/collab.type';
|
import { FieldId, RowId } from '@/application/collab.type';
|
||||||
import { DateFormat, TimeFormat } from '@/application/database-yjs';
|
import { DateFormat, TimeFormat } from '@/application/database-yjs/index';
|
||||||
import { FieldType } from '@/application/database-yjs/database.type';
|
import { FieldType } from '@/application/database-yjs/database.type';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { YArray } from 'yjs/dist/src/types/YArray';
|
import { YArray } from 'yjs/dist/src/types/YArray';
|
@ -18,7 +18,15 @@ export const getCellData = (rowId: string, fieldId: string, rowMetas: Y.Map<YDoc
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const metaIdFromRowId = (rowId: string) => {
|
export const metaIdFromRowId = (rowId: string) => {
|
||||||
const namespace = uuidParse(rowId);
|
let namespace: Uint8Array;
|
||||||
|
|
||||||
|
try {
|
||||||
|
namespace = uuidParse(rowId);
|
||||||
|
} catch (e) {
|
||||||
|
namespace = uuidParse(generateUUID());
|
||||||
|
}
|
||||||
|
|
||||||
return (key: RowMetaKey) => uuidv5(key, namespace).toString();
|
return (key: RowMetaKey) => uuidv5(key, namespace).toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateUUID = () => uuidv5(Date.now().toString(), uuidv5.URL);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||||
import { Row } from '@/application/database-yjs/selector';
|
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
@ -72,17 +71,3 @@ export function useDatabaseFields() {
|
|||||||
|
|
||||||
return database.get(YjsDatabaseKey.fields);
|
return database.get(YjsDatabaseKey.fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RowsState {
|
|
||||||
rowOrders: Row[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RowsContext = createContext<RowsState | null>(null);
|
|
||||||
|
|
||||||
export function useRowsContext() {
|
|
||||||
return useContext(RowsContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRows() {
|
|
||||||
return useRowsContext()?.rowOrders;
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import { getTimeFormat, getDateFormat } from './utils';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
import { DateFormat, TimeFormat } from '@/application/database-yjs';
|
||||||
|
|
||||||
|
describe('DateFormat', () => {
|
||||||
|
it('should return time format', () => {
|
||||||
|
expect(getTimeFormat(TimeFormat.TwelveHour)).toEqual('h:mm A');
|
||||||
|
expect(getTimeFormat(TimeFormat.TwentyFourHour)).toEqual('HH:mm');
|
||||||
|
expect(getTimeFormat(56)).toEqual('HH:mm');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return date format', () => {
|
||||||
|
expect(getDateFormat(DateFormat.US)).toEqual('YYYY/MM/DD');
|
||||||
|
expect(getDateFormat(DateFormat.ISO)).toEqual('YYYY-MM-DD');
|
||||||
|
expect(getDateFormat(DateFormat.Friendly)).toEqual('MMM DD, YYYY');
|
||||||
|
expect(getDateFormat(DateFormat.Local)).toEqual('MM/DD/YYYY');
|
||||||
|
expect(getDateFormat(DateFormat.DayMonthYear)).toEqual('DD/MM/YYYY');
|
||||||
|
|
||||||
|
expect(getDateFormat(56)).toEqual('YYYY-MM-DD');
|
||||||
|
});
|
||||||
|
});
|
@ -78,6 +78,9 @@ function createPredicate(conditions: ((row: Row) => boolean)[]) {
|
|||||||
|
|
||||||
export function filterBy(rows: Row[], filters: YDatabaseFilters, fields: YDatabaseFields, rowMetas: Y.Map<YDoc>) {
|
export function filterBy(rows: Row[], filters: YDatabaseFilters, fields: YDatabaseFields, rowMetas: Y.Map<YDoc>) {
|
||||||
const filterArray = filters.toArray();
|
const filterArray = filters.toArray();
|
||||||
|
|
||||||
|
if (filterArray.length === 0 || rowMetas.size === 0 || fields.size === 0) return rows;
|
||||||
|
|
||||||
const conditions = filterArray.map((filter) => {
|
const conditions = filterArray.map((filter) => {
|
||||||
return (row: { id: string }) => {
|
return (row: { id: string }) => {
|
||||||
const fieldId = filter.get(YjsDatabaseKey.field_id);
|
const fieldId = filter.get(YjsDatabaseKey.field_id);
|
||||||
@ -142,12 +145,12 @@ 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 (isNaN(Number(data)) || isNaN(Number(content)) || data === '' || content === '') {
|
||||||
if (condition === NumberFilterCondition.NumberIsEmpty && data === '') {
|
if (condition === NumberFilterCondition.NumberIsEmpty) {
|
||||||
return true;
|
return data === '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (condition === NumberFilterCondition.NumberIsNotEmpty && data !== '') {
|
if (condition === NumberFilterCondition.NumberIsNotEmpty) {
|
||||||
return true;
|
return data !== '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -169,10 +172,6 @@ export function numberFilterCheck(data: string, content: string, condition: numb
|
|||||||
return decimal < filterDecimal;
|
return decimal < filterDecimal;
|
||||||
case NumberFilterCondition.LessThanOrEqualTo:
|
case NumberFilterCondition.LessThanOrEqualTo:
|
||||||
return decimal <= filterDecimal;
|
return decimal <= filterDecimal;
|
||||||
case NumberFilterCondition.NumberIsEmpty:
|
|
||||||
return data === '';
|
|
||||||
case NumberFilterCondition.NumberIsNotEmpty:
|
|
||||||
return data !== '';
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -228,14 +227,6 @@ export function selectOptionFilterCheck(data: string, content: string, condition
|
|||||||
case SelectOptionFilterCondition.OptionDoesNotContain:
|
case SelectOptionFilterCondition.OptionDoesNotContain:
|
||||||
return some(filterOptionIds, (option) => !selectedOptionIds.includes(option));
|
return some(filterOptionIds, (option) => !selectedOptionIds.includes(option));
|
||||||
|
|
||||||
// Ensure selectedOptionIds is empty
|
|
||||||
case SelectOptionFilterCondition.OptionIsEmpty:
|
|
||||||
return selectedOptionIds.length === 0;
|
|
||||||
|
|
||||||
// Ensure selectedOptionIds is not empty
|
|
||||||
case SelectOptionFilterCondition.OptionIsNotEmpty:
|
|
||||||
return selectedOptionIds.length !== 0;
|
|
||||||
|
|
||||||
// Default case, if no conditions match
|
// Default case, if no conditions match
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
@ -14,18 +14,17 @@ import {
|
|||||||
useDatabaseView,
|
useDatabaseView,
|
||||||
useIsDatabaseRowPage,
|
useIsDatabaseRowPage,
|
||||||
useRowDocMap,
|
useRowDocMap,
|
||||||
useRows,
|
|
||||||
useViewId,
|
useViewId,
|
||||||
} from '@/application/database-yjs/context';
|
} from '@/application/database-yjs/context';
|
||||||
import { filterBy, parseFilter } from '@/application/database-yjs/filter';
|
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 { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
|
import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse';
|
||||||
import { DateTimeCell } from '@/components/database/components/cell/cell.type';
|
import { DateTimeCell } from '@/application/database-yjs/cell.type';
|
||||||
import dayjs from 'dayjs';
|
import * as dayjs from 'dayjs';
|
||||||
import { throttle } from 'lodash-es';
|
import { throttle } from 'lodash-es';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import Y from 'yjs';
|
import Y from 'yjs';
|
||||||
import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMetaKey, SortCondition } from './database.type';
|
import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMetaKey, SortCondition } from './database.type';
|
||||||
|
|
||||||
@ -149,12 +148,6 @@ export function useFieldsSelector(visibilitys: FieldVisibility[] = defaultVisibl
|
|||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRowsSelector() {
|
|
||||||
const rowOrders = useRows();
|
|
||||||
|
|
||||||
return useMemo(() => rowOrders ?? [], [rowOrders]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useFieldSelector(fieldId: string) {
|
export function useFieldSelector(fieldId: string) {
|
||||||
const database = useDatabase();
|
const database = useDatabase();
|
||||||
const [field, setField] = useState<YDatabaseField | null>(null);
|
const [field, setField] = useState<YDatabaseField | null>(null);
|
||||||
@ -403,7 +396,7 @@ export function useRowsByGroup(groupId: string) {
|
|||||||
if (!fieldId || !rowOrders || !rows) return;
|
if (!fieldId || !rowOrders || !rows) return;
|
||||||
|
|
||||||
const onConditionsChange = () => {
|
const onConditionsChange = () => {
|
||||||
if (rows.size !== rowOrders?.length) return;
|
if (rows.size < rowOrders?.length) return;
|
||||||
|
|
||||||
const newResult = new Map<string, Row[]>();
|
const newResult = new Map<string, Row[]>();
|
||||||
|
|
||||||
@ -456,7 +449,7 @@ export function useRowOrdersSelector() {
|
|||||||
|
|
||||||
if (!originalRowOrders || !rows) return;
|
if (!originalRowOrders || !rows) return;
|
||||||
|
|
||||||
if (originalRowOrders.length !== rows.size && !isDatabaseRowPage) return;
|
if (originalRowOrders.length > rows.size && !isDatabaseRowPage) return;
|
||||||
if (sorts?.length === 0 && filters?.length === 0) {
|
if (sorts?.length === 0 && filters?.length === 0) {
|
||||||
setRowOrders(originalRowOrders);
|
setRowOrders(originalRowOrders);
|
||||||
return;
|
return;
|
||||||
@ -691,7 +684,7 @@ export function useCalendarLayoutSetting() {
|
|||||||
|
|
||||||
export function usePrimaryFieldId() {
|
export function usePrimaryFieldId() {
|
||||||
const database = useDatabase();
|
const database = useDatabase();
|
||||||
const [primaryFieldId, setPrimaryFieldId] = React.useState<string | null>(null);
|
const [primaryFieldId, setPrimaryFieldId] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fields = database?.get(YjsDatabaseKey.fields);
|
const fields = database?.get(YjsDatabaseKey.fields);
|
||||||
|
@ -15,6 +15,8 @@ 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>) {
|
||||||
const sortArray = sorts.toArray();
|
const sortArray = sorts.toArray();
|
||||||
|
|
||||||
|
if (sortArray.length === 0 || rowMetas.size === 0 || fields.size === 0) return rows;
|
||||||
const iteratees = sortArray.map((sort) => {
|
const iteratees = sortArray.map((sort) => {
|
||||||
return (row: { id: string }) => {
|
return (row: { id: string }) => {
|
||||||
const fieldId = sort.get(YjsDatabaseKey.field_id);
|
const fieldId = sort.get(YjsDatabaseKey.field_id);
|
||||||
@ -26,8 +28,7 @@ export function sortBy(rows: Row[], sorts: YDatabaseSorts, fields: YDatabaseFiel
|
|||||||
|
|
||||||
const defaultData = parseCellDataForSort(field, '');
|
const defaultData = parseCellDataForSort(field, '');
|
||||||
|
|
||||||
if (!rowMeta) return defaultData;
|
const meta = rowMeta?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow;
|
||||||
const meta = rowMeta.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow;
|
|
||||||
|
|
||||||
if (!meta) return defaultData;
|
if (!meta) return defaultData;
|
||||||
if (fieldType === FieldType.LastEditedTime) {
|
if (fieldType === FieldType.LastEditedTime) {
|
||||||
@ -69,9 +70,9 @@ export function parseCellDataForSort(field: YDatabaseField, data: string | boole
|
|||||||
return data === 'Yes';
|
return data === 'Yes';
|
||||||
case FieldType.SingleSelect:
|
case FieldType.SingleSelect:
|
||||||
case FieldType.MultiSelect:
|
case FieldType.MultiSelect:
|
||||||
return parseSelectOptionCellData(field, typeof data === 'string' ? data : '');
|
return parseSelectOptionCellData(field, data as string);
|
||||||
case FieldType.Checklist:
|
case FieldType.Checklist:
|
||||||
return parseChecklistData(typeof data === 'string' ? data : '')?.percentage ?? 0;
|
return parseChecklistData(data as string)?.percentage ?? 0;
|
||||||
case FieldType.DateTime:
|
case FieldType.DateTime:
|
||||||
return Number(data);
|
return Number(data);
|
||||||
case FieldType.Relation:
|
case FieldType.Relation:
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
import { CollabOrigin } from '@/application/collab.type';
|
||||||
|
import { yDocToSlateContent } from '@/application/slate-yjs/utils/convert';
|
||||||
|
import { generateId, insertBlock, 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
|
||||||
|
const id = generateId();
|
||||||
|
|
||||||
|
const { applyDelta } = insertBlock({
|
||||||
|
doc: remoteDoc,
|
||||||
|
blockObject: {
|
||||||
|
id,
|
||||||
|
ty: 'paragraph',
|
||||||
|
relation_id: id,
|
||||||
|
text_id: id,
|
||||||
|
data: JSON.stringify({ level: 1 }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
applyDelta([{ insert: 'Hello ' }, { insert: 'World', attributes: { bold: true } }]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
@ -0,0 +1,268 @@
|
|||||||
|
import { generateId, getTestingDocData, insertBlock, withTestingYDoc } from './withTestingYjsEditor';
|
||||||
|
import { yDocToSlateContent, deltaInsertToSlateNode, yDataToSlateContent } from '@/application/slate-yjs/utils/convert';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
|
describe('convert yjs data to slate content', () => {
|
||||||
|
it('should return undefined if root block is not exist', () => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
|
||||||
|
expect(() => yDocToSlateContent(doc)).toThrowError();
|
||||||
|
|
||||||
|
const doc2 = withTestingYDoc('1');
|
||||||
|
const { blocks, childrenMap, textMap, pageId } = getTestingDocData(doc2);
|
||||||
|
expect(yDataToSlateContent({ blocks, rootId: '2', childrenMap, textMap })).toBeUndefined();
|
||||||
|
|
||||||
|
blocks.delete(pageId);
|
||||||
|
|
||||||
|
expect(yDataToSlateContent({ blocks, rootId: pageId, childrenMap, textMap })).toBeUndefined();
|
||||||
|
});
|
||||||
|
it('should match empty array', () => {
|
||||||
|
const doc = withTestingYDoc('1');
|
||||||
|
const slateContent = yDocToSlateContent(doc)!;
|
||||||
|
|
||||||
|
expect(slateContent).not.toBeUndefined();
|
||||||
|
expect(slateContent.children).toMatchObject([]);
|
||||||
|
});
|
||||||
|
it('should match single paragraph', () => {
|
||||||
|
const doc = withTestingYDoc('1');
|
||||||
|
const id = generateId();
|
||||||
|
|
||||||
|
const { applyDelta } = insertBlock({
|
||||||
|
doc,
|
||||||
|
blockObject: {
|
||||||
|
id,
|
||||||
|
ty: 'paragraph',
|
||||||
|
relation_id: id,
|
||||||
|
text_id: id,
|
||||||
|
data: JSON.stringify({ level: 1 }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
applyDelta([{ insert: 'Hello ' }, { insert: 'World', attributes: { bold: true } }]);
|
||||||
|
const slateContent = yDocToSlateContent(doc)!;
|
||||||
|
|
||||||
|
expect(slateContent).not.toBeUndefined();
|
||||||
|
expect(slateContent.children).toEqual([
|
||||||
|
{
|
||||||
|
blockId: id,
|
||||||
|
relationId: id,
|
||||||
|
type: 'paragraph',
|
||||||
|
data: { level: 1 },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
textId: id,
|
||||||
|
type: 'text',
|
||||||
|
children: [{ text: 'Hello ' }, { text: 'World', bold: true }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('should match nesting paragraphs', () => {
|
||||||
|
const doc = withTestingYDoc('1');
|
||||||
|
const id1 = generateId();
|
||||||
|
const id2 = generateId();
|
||||||
|
|
||||||
|
const { applyDelta, appendChild } = insertBlock({
|
||||||
|
doc,
|
||||||
|
blockObject: {
|
||||||
|
id: id1,
|
||||||
|
ty: 'paragraph',
|
||||||
|
relation_id: id1,
|
||||||
|
text_id: id1,
|
||||||
|
data: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
applyDelta([{ insert: 'Hello ' }, { insert: 'World', attributes: { bold: true } }]);
|
||||||
|
appendChild({
|
||||||
|
id: id2,
|
||||||
|
ty: 'paragraph',
|
||||||
|
relation_id: id2,
|
||||||
|
text_id: id2,
|
||||||
|
data: '',
|
||||||
|
}).applyDelta([{ insert: 'I am nested' }]);
|
||||||
|
|
||||||
|
const slateContent = yDocToSlateContent(doc)!;
|
||||||
|
|
||||||
|
expect(slateContent).not.toBeUndefined();
|
||||||
|
expect(slateContent.children).toEqual([
|
||||||
|
{
|
||||||
|
blockId: id1,
|
||||||
|
relationId: id1,
|
||||||
|
type: 'paragraph',
|
||||||
|
data: {},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
textId: id1,
|
||||||
|
type: 'text',
|
||||||
|
children: [{ text: 'Hello ' }, { text: 'World', bold: true }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockId: id2,
|
||||||
|
relationId: id2,
|
||||||
|
type: 'paragraph',
|
||||||
|
data: {},
|
||||||
|
children: [{ textId: id2, type: 'text', children: [{ text: 'I am nested' }] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('should compatible with delta in data', () => {
|
||||||
|
const doc = withTestingYDoc('1');
|
||||||
|
const id = generateId();
|
||||||
|
|
||||||
|
insertBlock({
|
||||||
|
doc,
|
||||||
|
blockObject: {
|
||||||
|
id,
|
||||||
|
ty: 'paragraph',
|
||||||
|
relation_id: id,
|
||||||
|
text_id: id,
|
||||||
|
data: JSON.stringify({
|
||||||
|
delta: [
|
||||||
|
{ insert: 'Hello ' },
|
||||||
|
{ insert: 'World', attributes: { bold: true } },
|
||||||
|
{ insert: ' ', attributes: { code: true } },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const slateContent = yDocToSlateContent(doc)!;
|
||||||
|
|
||||||
|
expect(slateContent).not.toBeUndefined();
|
||||||
|
expect(slateContent.children).toEqual([
|
||||||
|
{
|
||||||
|
blockId: id,
|
||||||
|
relationId: id,
|
||||||
|
type: 'paragraph',
|
||||||
|
data: {
|
||||||
|
delta: [
|
||||||
|
{ insert: 'Hello ' },
|
||||||
|
{ insert: 'World', attributes: { bold: true } },
|
||||||
|
{
|
||||||
|
insert: ' ',
|
||||||
|
attributes: { code: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
textId: id,
|
||||||
|
type: 'text',
|
||||||
|
children: [{ text: 'Hello ' }, { text: 'World', bold: true }, { text: ' ', code: true }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('should return undefined if data is invalid', () => {
|
||||||
|
const doc = withTestingYDoc('1');
|
||||||
|
const id = generateId();
|
||||||
|
|
||||||
|
insertBlock({
|
||||||
|
doc,
|
||||||
|
blockObject: {
|
||||||
|
id,
|
||||||
|
ty: 'paragraph',
|
||||||
|
relation_id: id,
|
||||||
|
text_id: id,
|
||||||
|
data: 'invalid',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const slateContent = yDocToSlateContent(doc)!;
|
||||||
|
|
||||||
|
expect(slateContent).not.toBeUndefined();
|
||||||
|
expect(slateContent.children).toEqual([undefined]);
|
||||||
|
});
|
||||||
|
it('should return a normalize node if the delta is not exist', () => {
|
||||||
|
const doc = withTestingYDoc('1');
|
||||||
|
const id = generateId();
|
||||||
|
|
||||||
|
insertBlock({
|
||||||
|
doc,
|
||||||
|
blockObject: {
|
||||||
|
id,
|
||||||
|
ty: 'paragraph',
|
||||||
|
relation_id: id,
|
||||||
|
text_id: id,
|
||||||
|
data: JSON.stringify({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const slateContent = yDocToSlateContent(doc)!;
|
||||||
|
|
||||||
|
expect(slateContent).not.toBeUndefined();
|
||||||
|
expect(slateContent.children).toEqual([
|
||||||
|
{
|
||||||
|
blockId: id,
|
||||||
|
relationId: id,
|
||||||
|
type: 'paragraph',
|
||||||
|
data: {},
|
||||||
|
children: [{ text: '' }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('test deltaInsertToSlateNode', () => {
|
||||||
|
it('should match text node', () => {
|
||||||
|
const node = deltaInsertToSlateNode({ insert: 'Hello' });
|
||||||
|
|
||||||
|
expect(node).toEqual({ text: 'Hello' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match text node with attributes', () => {
|
||||||
|
const node = deltaInsertToSlateNode({ insert: 'Hello', attributes: { bold: true } });
|
||||||
|
|
||||||
|
expect(node).toEqual({ text: 'Hello', bold: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete empty string attributes', () => {
|
||||||
|
const node = deltaInsertToSlateNode({ insert: 'Hello', attributes: { bold: false, font_color: '' } });
|
||||||
|
|
||||||
|
expect(node).toEqual({ text: 'Hello' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate formula inline node', () => {
|
||||||
|
const node = deltaInsertToSlateNode({
|
||||||
|
insert: '$$',
|
||||||
|
attributes: { formula: 'world' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(node).toEqual([
|
||||||
|
{
|
||||||
|
type: 'formula',
|
||||||
|
data: 'world',
|
||||||
|
children: [{ text: '$' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'formula',
|
||||||
|
data: 'world',
|
||||||
|
children: [{ text: '$' }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate mention inline node', () => {
|
||||||
|
const node = deltaInsertToSlateNode({
|
||||||
|
insert: '@',
|
||||||
|
attributes: { mention: 'world' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(node).toEqual([
|
||||||
|
{
|
||||||
|
type: 'mention',
|
||||||
|
data: 'world',
|
||||||
|
children: [{ text: '@' }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,5 @@
|
|||||||
import { withTestingYDoc, withTestingYjsEditor } from './withTestingYjsEditor';
|
import { withTestingYDoc, withTestingYjsEditor } from './withTestingYjsEditor';
|
||||||
import { yDocToSlateContent } from '../convert';
|
import { yDocToSlateContent } from '../utils/convert';
|
||||||
import { createEditor, Editor } from 'slate';
|
import { createEditor, Editor } from 'slate';
|
||||||
import { expect } from '@jest/globals';
|
import { expect } from '@jest/globals';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
@ -39,3 +39,34 @@ export async function runCollaborationTest() {
|
|||||||
expect(yjsEditor.children).toEqual(remote.children);
|
expect(yjsEditor.children).toEqual(remote.children);
|
||||||
expect(normalizedSlateDoc(doc)).toEqual(yjsEditor.children);
|
expect(normalizedSlateDoc(doc)).toEqual(yjsEditor.children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function runLocalChangeTest() {
|
||||||
|
const doc = withTestingYDoc('1');
|
||||||
|
const editor = withTestingYjsEditor(createEditor(), doc);
|
||||||
|
|
||||||
|
editor.connect();
|
||||||
|
|
||||||
|
editor.insertNode(
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
blockId: '1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
textId: '1',
|
||||||
|
type: 'text',
|
||||||
|
children: [{ text: 'Hello' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
at: [0],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
editor.apply({
|
||||||
|
type: 'set_selection',
|
||||||
|
properties: {},
|
||||||
|
newProperties: { anchor: { path: [0, 0], offset: 5 }, focus: { path: [0, 0], offset: 5 } },
|
||||||
|
});
|
||||||
|
// expect(editor.children).toEqual(yDocToSlateContent(doc)?.children);
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
import { runCollaborationTest, runLocalChangeTest } from './convert';
|
||||||
|
import { runApplyRemoteEventsTest } from './applyRemoteEvents';
|
||||||
|
import {
|
||||||
|
getTestingDocData,
|
||||||
|
withTestingYDoc,
|
||||||
|
withTestingYjsEditor,
|
||||||
|
} from '@/application/slate-yjs/__tests__/withTestingYjsEditor';
|
||||||
|
import { createEditor } from 'slate';
|
||||||
|
import Y from 'yjs';
|
||||||
|
import { expect } from '@jest/globals';
|
||||||
|
import { YjsEditor } from '@/application/slate-yjs';
|
||||||
|
|
||||||
|
describe('slate-yjs adapter', () => {
|
||||||
|
it('should pass the collaboration test', async () => {
|
||||||
|
await runCollaborationTest();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass the apply remote events test', async () => {
|
||||||
|
await runApplyRemoteEventsTest();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store local changes', () => {
|
||||||
|
runLocalChangeTest();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when already connected', () => {
|
||||||
|
const doc = withTestingYDoc('1');
|
||||||
|
const editor = withTestingYjsEditor(createEditor(), doc);
|
||||||
|
editor.connect();
|
||||||
|
expect(() => editor.connect()).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should re connect after disconnect', () => {
|
||||||
|
const doc = withTestingYDoc('1');
|
||||||
|
const editor = withTestingYjsEditor(createEditor(), doc);
|
||||||
|
editor.connect();
|
||||||
|
editor.disconnect();
|
||||||
|
expect(() => editor.connect()).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ensure the editor is connected before disconnecting', () => {
|
||||||
|
const doc = withTestingYDoc('1');
|
||||||
|
const editor = withTestingYjsEditor(createEditor(), doc);
|
||||||
|
expect(() => editor.disconnect()).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have been called', () => {
|
||||||
|
const doc = withTestingYDoc('1');
|
||||||
|
const editor = withTestingYjsEditor(createEditor(), doc);
|
||||||
|
editor.connect = jest.fn();
|
||||||
|
YjsEditor.connect(editor);
|
||||||
|
expect(editor.connect).toHaveBeenCalled();
|
||||||
|
|
||||||
|
editor.disconnect = jest.fn();
|
||||||
|
YjsEditor.disconnect(editor);
|
||||||
|
expect(editor.disconnect).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should can not be converted to slate content', () => {
|
||||||
|
const doc = withTestingYDoc('1');
|
||||||
|
const { blocks, childrenMap, textMap, pageId } = getTestingDocData(doc);
|
||||||
|
blocks.delete(pageId);
|
||||||
|
const editor = withTestingYjsEditor(createEditor(), doc);
|
||||||
|
YjsEditor.connect(editor);
|
||||||
|
expect(editor.children).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,135 @@
|
|||||||
|
import {
|
||||||
|
CollabOrigin,
|
||||||
|
YBlocks,
|
||||||
|
YChildrenMap,
|
||||||
|
YjsEditorKey,
|
||||||
|
YMeta,
|
||||||
|
YSharedRoot,
|
||||||
|
YTextMap,
|
||||||
|
} from '@/application/collab.type';
|
||||||
|
import { withYjs } from '@/application/slate-yjs';
|
||||||
|
import { YDelta } from '@/application/slate-yjs/utils/convert';
|
||||||
|
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 getTestingDocData(doc: Y.Doc) {
|
||||||
|
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 pageId = document.get(YjsEditorKey.page_id) as string;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sharedRoot,
|
||||||
|
document,
|
||||||
|
blocks,
|
||||||
|
meta,
|
||||||
|
childrenMap,
|
||||||
|
textMap,
|
||||||
|
pageId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlockObject {
|
||||||
|
id: string;
|
||||||
|
ty: string;
|
||||||
|
relation_id: string;
|
||||||
|
text_id: string;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insertBlock({
|
||||||
|
doc,
|
||||||
|
parentBlockId,
|
||||||
|
prevBlockId,
|
||||||
|
blockObject,
|
||||||
|
}: {
|
||||||
|
doc: Y.Doc;
|
||||||
|
parentBlockId?: string;
|
||||||
|
prevBlockId?: string;
|
||||||
|
blockObject: BlockObject;
|
||||||
|
}) {
|
||||||
|
const { blocks, childrenMap, textMap, pageId } = getTestingDocData(doc);
|
||||||
|
const block = new Y.Map();
|
||||||
|
const { id, ty, relation_id, text_id, data } = blockObject;
|
||||||
|
|
||||||
|
block.set(YjsEditorKey.block_id, id);
|
||||||
|
block.set(YjsEditorKey.block_type, ty);
|
||||||
|
block.set(YjsEditorKey.block_children, relation_id);
|
||||||
|
block.set(YjsEditorKey.block_external_id, text_id);
|
||||||
|
block.set(YjsEditorKey.block_data, data);
|
||||||
|
blocks.set(id, block);
|
||||||
|
|
||||||
|
const blockParentId = parentBlockId || pageId;
|
||||||
|
const blockParentChildren = childrenMap.get(blockParentId);
|
||||||
|
const index = prevBlockId ? blockParentChildren.toArray().indexOf(prevBlockId) + 1 : 0;
|
||||||
|
|
||||||
|
blockParentChildren.insert(index, [id]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
applyDelta: (delta: YDelta[]) => {
|
||||||
|
let text = textMap.get(text_id);
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
text = new Y.Text();
|
||||||
|
textMap.set(text_id, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
text.applyDelta(delta);
|
||||||
|
},
|
||||||
|
appendChild: (childBlock: BlockObject) => {
|
||||||
|
if (!childrenMap.has(relation_id)) {
|
||||||
|
childrenMap.set(relation_id, new Y.Array());
|
||||||
|
}
|
||||||
|
|
||||||
|
return insertBlock({
|
||||||
|
doc,
|
||||||
|
parentBlockId: id,
|
||||||
|
blockObject: childBlock,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -1,67 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,45 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -11,21 +11,19 @@ import {
|
|||||||
BlockType,
|
BlockType,
|
||||||
} from '@/application/collab.type';
|
} from '@/application/collab.type';
|
||||||
import { BlockJson } from '@/application/slate-yjs/utils/types';
|
import { BlockJson } from '@/application/slate-yjs/utils/types';
|
||||||
import { getFontFamily } from '@/utils/font';
|
|
||||||
import { uniq } from 'lodash-es';
|
|
||||||
import { Element, Text } from 'slate';
|
import { Element, Text } from 'slate';
|
||||||
|
|
||||||
export function yDocToSlateContent(doc: YDoc): Element | undefined {
|
export function yDataToSlateContent({
|
||||||
const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
blocks,
|
||||||
|
rootId,
|
||||||
const document = sharedRoot.get(YjsEditorKey.document);
|
childrenMap,
|
||||||
const pageId = document.get(YjsEditorKey.page_id) as string;
|
textMap,
|
||||||
const blocks = document.get(YjsEditorKey.blocks) as YBlocks;
|
}: {
|
||||||
const meta = document.get(YjsEditorKey.meta) as YMeta;
|
blocks: YBlocks;
|
||||||
const childrenMap = meta.get(YjsEditorKey.children_map) as YChildrenMap;
|
childrenMap: YChildrenMap;
|
||||||
const textMap = meta.get(YjsEditorKey.text_map) as YTextMap;
|
textMap: YTextMap;
|
||||||
const fontFamilys: string[] = [];
|
rootId: string;
|
||||||
|
}): Element | undefined {
|
||||||
function traverse(id: string) {
|
function traverse(id: string) {
|
||||||
const block = blocks.get(id).toJSON() as BlockJson;
|
const block = blocks.get(id).toJSON() as BlockJson;
|
||||||
const childrenId = block.children as string;
|
const childrenId = block.children as string;
|
||||||
@ -44,7 +42,9 @@ export function yDocToSlateContent(doc: YDoc): Element | undefined {
|
|||||||
|
|
||||||
let delta;
|
let delta;
|
||||||
|
|
||||||
if (!textId) {
|
const yText = textId ? textMap.get(textId) : undefined;
|
||||||
|
|
||||||
|
if (!yText) {
|
||||||
if (children.length === 0) {
|
if (children.length === 0) {
|
||||||
children.push({
|
children.push({
|
||||||
text: '',
|
text: '',
|
||||||
@ -64,18 +64,12 @@ export function yDocToSlateContent(doc: YDoc): Element | undefined {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
delta = textMap.get(textId)?.toDelta();
|
delta = yText.toDelta();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const slateDelta = delta.flatMap(deltaInsertToSlateNode);
|
const slateDelta = delta.flatMap(deltaInsertToSlateNode);
|
||||||
|
|
||||||
// collect font family
|
|
||||||
slateDelta.forEach((node: Text) => {
|
|
||||||
if (node.font_family) {
|
|
||||||
fontFamilys.push(getFontFamily(node.font_family));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const textNode: Element = {
|
const textNode: Element = {
|
||||||
textId,
|
textId,
|
||||||
type: YjsEditorKey.text,
|
type: YjsEditorKey.text,
|
||||||
@ -85,30 +79,39 @@ export function yDocToSlateContent(doc: YDoc): Element | undefined {
|
|||||||
children.unshift(textNode);
|
children.unshift(textNode);
|
||||||
return slateNode;
|
return slateNode;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = blocks.get(pageId);
|
const root = blocks.get(rootId);
|
||||||
|
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
|
|
||||||
const result = traverse(pageId);
|
const result = traverse(rootId);
|
||||||
|
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
if (fontFamilys.length > 0) {
|
|
||||||
window.WebFont?.load({
|
|
||||||
google: {
|
|
||||||
families: uniq(fontFamilys),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function yDocToSlateContent(doc: YDoc): Element | undefined {
|
||||||
|
const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
||||||
|
|
||||||
|
const document = sharedRoot.get(YjsEditorKey.document);
|
||||||
|
const pageId = document.get(YjsEditorKey.page_id) as string;
|
||||||
|
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;
|
||||||
|
|
||||||
|
return yDataToSlateContent({
|
||||||
|
blocks,
|
||||||
|
rootId: pageId,
|
||||||
|
childrenMap,
|
||||||
|
textMap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function blockToSlateNode(block: BlockJson): Element {
|
export function blockToSlateNode(block: BlockJson): Element {
|
||||||
const data = block.data;
|
const data = block.data;
|
||||||
let blockData;
|
let blockData;
|
||||||
@ -116,7 +119,7 @@ export function blockToSlateNode(block: BlockJson): Element {
|
|||||||
try {
|
try {
|
||||||
blockData = data ? JSON.parse(data) : {};
|
blockData = data ? JSON.parse(data) : {};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
blockData = {};
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -128,13 +131,12 @@ export function blockToSlateNode(block: BlockJson): Element {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deltaInsertToSlateNode({
|
export interface YDelta {
|
||||||
attributes,
|
|
||||||
insert,
|
|
||||||
}: {
|
|
||||||
insert: string;
|
insert: string;
|
||||||
attributes: Record<string, string | number | undefined | boolean>;
|
attributes?: Record<string, string | number | undefined | boolean>;
|
||||||
}): Element | Text | Element[] {
|
}
|
||||||
|
|
||||||
|
export function deltaInsertToSlateNode({ attributes, insert }: YDelta): Element | Text | Element[] {
|
||||||
const matchInlines = transformToInlineElement({
|
const matchInlines = transformToInlineElement({
|
||||||
insert,
|
insert,
|
||||||
attributes,
|
attributes,
|
||||||
@ -145,17 +147,7 @@ export function deltaInsertToSlateNode({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (attributes) {
|
if (attributes) {
|
||||||
if ('font_color' in attributes && attributes['font_color'] === '') {
|
dealWithEmptyAttribute(attributes);
|
||||||
delete attributes['font_color'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('bg_color' in attributes && attributes['bg_color'] === '') {
|
|
||||||
delete attributes['bg_color'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('code' in attributes && !attributes['code']) {
|
|
||||||
delete attributes['code'];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -164,10 +156,15 @@ export function deltaInsertToSlateNode({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformToInlineElement(op: {
|
function dealWithEmptyAttribute(attributes: Record<string, string | number | undefined | boolean>) {
|
||||||
insert: string;
|
for (const key in attributes) {
|
||||||
attributes: Record<string, string | number | undefined | boolean>;
|
if (!attributes[key]) {
|
||||||
}): Element[] {
|
delete attributes[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformToInlineElement(op: YDelta): Element[] {
|
||||||
const attributes = op.attributes;
|
const attributes = op.attributes;
|
||||||
|
|
||||||
if (!attributes) return [];
|
if (!attributes) return [];
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useCellSelector } from '@/application/database-yjs';
|
import { useCellSelector } from '@/application/database-yjs';
|
||||||
import { TextCell } from '@/components/database/components/cell/cell.type';
|
import { TextCell } from '@/application/database-yjs/cell.type';
|
||||||
import { TextProperty } from '@/components/database/components/property/text';
|
import { TextProperty } from '@/components/database/components/property/text';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import { CheckboxCell } from '@/components/database/components/cell/checkbox';
|
|||||||
import { SelectOptionCell } from '@/components/database/components/cell/select-option';
|
import { SelectOptionCell } from '@/components/database/components/cell/select-option';
|
||||||
import { DateTimeCell } from '@/components/database/components/cell/date';
|
import { DateTimeCell } from '@/components/database/components/cell/date';
|
||||||
import { ChecklistCell } from '@/components/database/components/cell/checklist';
|
import { ChecklistCell } from '@/components/database/components/cell/checklist';
|
||||||
import { CellProps, Cell as CellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, Cell as CellType } from '@/application/database-yjs/cell.type';
|
||||||
import { RelationCell } from '@/components/database/components/cell/relation';
|
import { RelationCell } from '@/components/database/components/cell/relation';
|
||||||
|
|
||||||
export function Cell(props: CellProps<CellType>) {
|
export function Cell(props: CellProps<CellType>) {
|
||||||
|
@ -11,15 +11,3 @@ export const SelectOptionColorMap = {
|
|||||||
[SelectOptionColor.Aqua]: '--tint-aqua',
|
[SelectOptionColor.Aqua]: '--tint-aqua',
|
||||||
[SelectOptionColor.Blue]: '--tint-blue',
|
[SelectOptionColor.Blue]: '--tint-blue',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectOptionColorTextMap = {
|
|
||||||
[SelectOptionColor.Purple]: 'purpleColor',
|
|
||||||
[SelectOptionColor.Pink]: 'pinkColor',
|
|
||||||
[SelectOptionColor.LightPink]: 'lightPinkColor',
|
|
||||||
[SelectOptionColor.Orange]: 'orangeColor',
|
|
||||||
[SelectOptionColor.Yellow]: 'yellowColor',
|
|
||||||
[SelectOptionColor.Lime]: 'limeColor',
|
|
||||||
[SelectOptionColor.Green]: 'greenColor',
|
|
||||||
[SelectOptionColor.Aqua]: 'aquaColor',
|
|
||||||
[SelectOptionColor.Blue]: 'blueColor',
|
|
||||||
} as const;
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';
|
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';
|
||||||
import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg';
|
import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg';
|
||||||
import { FieldType } from '@/application/database-yjs';
|
import { FieldType } from '@/application/database-yjs';
|
||||||
import { CellProps, CheckboxCell as CheckboxCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, CheckboxCell as CheckboxCellType } from '@/application/database-yjs/cell.type';
|
||||||
|
|
||||||
export function CheckboxCell({ cell, style }: CellProps<CheckboxCellType>) {
|
export function CheckboxCell({ cell, style }: CellProps<CheckboxCellType>) {
|
||||||
const checked = cell?.data;
|
const checked = cell?.data;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FieldType, parseChecklistData } from '@/application/database-yjs';
|
import { FieldType, parseChecklistData } from '@/application/database-yjs';
|
||||||
import { CellProps, ChecklistCell as ChecklistCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, ChecklistCell as ChecklistCellType } from '@/application/database-yjs/cell.type';
|
||||||
import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel';
|
import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { FieldType } from '@/application/database-yjs';
|
import { FieldType } from '@/application/database-yjs';
|
||||||
import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks';
|
import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks';
|
||||||
import { CellProps, DateTimeCell as DateTimeCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, DateTimeCell as DateTimeCellType } from '@/application/database-yjs/cell.type';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg';
|
import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg';
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
parseNumberTypeOptions,
|
parseNumberTypeOptions,
|
||||||
FieldType,
|
FieldType,
|
||||||
} from '@/application/database-yjs';
|
} from '@/application/database-yjs';
|
||||||
import { CellProps, NumberCell as NumberCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, NumberCell as NumberCellType } from '@/application/database-yjs/cell.type';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import Decimal from 'decimal.js';
|
import Decimal from 'decimal.js';
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useNavigateToRow, useRowMetaSelector } from '@/application/database-yjs';
|
import { useNavigateToRow, useRowMetaSelector } from '@/application/database-yjs';
|
||||||
import { TextCell as CellType, CellProps } from '@/components/database/components/cell/cell.type';
|
import { TextCell as CellType, CellProps } from '@/application/database-yjs/cell.type';
|
||||||
import { TextCell } from '@/components/database/components/cell/text';
|
import { TextCell } from '@/components/database/components/cell/text';
|
||||||
import OpenAction from '@/components/database/components/database-row/OpenAction';
|
import OpenAction from '@/components/database/components/database-row/OpenAction';
|
||||||
import { getPlatform } from '@/utils/platform';
|
import { getPlatform } from '@/utils/platform';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FieldType } from '@/application/database-yjs';
|
import { FieldType } from '@/application/database-yjs';
|
||||||
import { CellProps, RelationCell as RelationCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, RelationCell as RelationCellType } from '@/application/database-yjs/cell.type';
|
||||||
import RelationItems from '@/components/database/components/cell/relation/RelationItems';
|
import RelationItems from '@/components/database/components/cell/relation/RelationItems';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
useFieldSelector,
|
useFieldSelector,
|
||||||
useNavigateToRow,
|
useNavigateToRow,
|
||||||
} from '@/application/database-yjs';
|
} from '@/application/database-yjs';
|
||||||
import { RelationCell, RelationCellData } from '@/components/database/components/cell/cell.type';
|
import { RelationCell, RelationCellData } from '@/application/database-yjs/cell.type';
|
||||||
import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue';
|
import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue';
|
||||||
import { useGetDatabaseDispatch } from '@/components/database/Database.hooks';
|
import { useGetDatabaseDispatch } from '@/components/database/Database.hooks';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FieldId, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
import { FieldId, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||||
import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
|
import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) {
|
export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useFieldSelector, parseSelectOptionTypeOptions } from '@/application/database-yjs';
|
import { useFieldSelector, parseSelectOptionTypeOptions } from '@/application/database-yjs';
|
||||||
import { Tag } from '@/components/_shared/tag';
|
import { Tag } from '@/components/_shared/tag';
|
||||||
import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const';
|
import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const';
|
||||||
import { CellProps, SelectOptionCell as SelectOptionCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, SelectOptionCell as SelectOptionCellType } from '@/application/database-yjs/cell.type';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
export function SelectOptionCell({ cell, fieldId, style, placeholder }: CellProps<SelectOptionCellType>) {
|
export function SelectOptionCell({ cell, fieldId, style, placeholder }: CellProps<SelectOptionCellType>) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useReadOnly } from '@/application/database-yjs';
|
import { useReadOnly } from '@/application/database-yjs';
|
||||||
import { CellProps, TextCell as TextCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, TextCell as TextCellType } from '@/application/database-yjs/cell.type';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export function TextCell({ cell, style, placeholder }: CellProps<TextCellType>) {
|
export function TextCell({ cell, style, placeholder }: CellProps<TextCellType>) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useReadOnly } from '@/application/database-yjs';
|
import { useReadOnly } from '@/application/database-yjs';
|
||||||
import { CellProps, UrlCell as UrlCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, UrlCell as UrlCellType } from '@/application/database-yjs/cell.type';
|
||||||
import { openUrl, processUrl } from '@/utils/url';
|
import { openUrl, processUrl } from '@/utils/url';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { FieldId, YjsDatabaseKey } from '@/application/collab.type';
|
|||||||
import { useCellSelector } from '@/application/database-yjs';
|
import { useCellSelector } from '@/application/database-yjs';
|
||||||
import { useFieldSelector } from '@/application/database-yjs/selector';
|
import { useFieldSelector } from '@/application/database-yjs/selector';
|
||||||
import { Cell } from '@/components/database/components/cell';
|
import { Cell } from '@/components/database/components/cell';
|
||||||
import { CellProps, Cell as CellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, Cell as CellType } from '@/application/database-yjs/cell.type';
|
||||||
import { PrimaryCell } from '@/components/database/components/cell/primary';
|
import { PrimaryCell } from '@/components/database/components/cell/primary';
|
||||||
import React, { useEffect, useMemo, useRef } from 'react';
|
import React, { useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DEFAULT_ROW_HEIGHT, useReadOnly, useRowsSelector } from '@/application/database-yjs';
|
import { DEFAULT_ROW_HEIGHT, useReadOnly, useRowOrdersSelector } from '@/application/database-yjs';
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
@ -15,16 +15,19 @@ export type RenderRow = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useRenderRows() {
|
export function useRenderRows() {
|
||||||
const rows = useRowsSelector();
|
const rows = useRowOrdersSelector();
|
||||||
const readOnly = useReadOnly();
|
const readOnly = useReadOnly();
|
||||||
|
|
||||||
const renderRows = useMemo(() => {
|
const renderRows = useMemo(() => {
|
||||||
return [
|
const rowItems =
|
||||||
...rows.map((row) => ({
|
rows?.map((row) => ({
|
||||||
type: RenderRowType.Row,
|
type: RenderRowType.Row,
|
||||||
rowId: row.id,
|
rowId: row.id,
|
||||||
height: row.height,
|
height: row.height,
|
||||||
})),
|
})) ?? [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
...rowItems,
|
||||||
|
|
||||||
!readOnly && {
|
!readOnly && {
|
||||||
type: RenderRowType.NewRow,
|
type: RenderRowType.NewRow,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { YjsDatabaseKey } from '@/application/collab.type';
|
import { YjsDatabaseKey } from '@/application/collab.type';
|
||||||
import { FieldType, useCellSelector, useFieldSelector } from '@/application/database-yjs';
|
import { FieldType, useCellSelector, useFieldSelector } from '@/application/database-yjs';
|
||||||
import { Cell as CellType, CellProps } from '@/components/database/components/cell/cell.type';
|
import { Cell as CellType, CellProps } from '@/application/database-yjs/cell.type';
|
||||||
import { CheckboxCell } from '@/components/database/components/cell/checkbox';
|
import { CheckboxCell } from '@/components/database/components/cell/checkbox';
|
||||||
import { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified';
|
import { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified';
|
||||||
import { DateTimeCell } from '@/components/database/components/cell/date';
|
import { DateTimeCell } from '@/components/database/components/cell/date';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { parseChecklistData } from '@/application/database-yjs';
|
import { parseChecklistData } from '@/application/database-yjs';
|
||||||
import { CellProps, ChecklistCell as CellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, ChecklistCell as CellType } from '@/application/database-yjs/cell.type';
|
||||||
import { ChecklistCell } from '@/components/database/components/cell/checklist';
|
import { ChecklistCell } from '@/components/database/components/cell/checklist';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';
|
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CellProps, TextCell } from '@/components/database/components/cell/cell.type';
|
import { CellProps, TextCell } from '@/application/database-yjs/cell.type';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { RowsContext, useDatabase, useRowOrdersSelector, useViewId } from '@/application/database-yjs';
|
import { useDatabase, useViewId } from '@/application/database-yjs';
|
||||||
import { useRenderFields, GridHeader, GridTable } from '@/components/database/components/grid';
|
import { useRenderFields, GridHeader, GridTable } from '@/components/database/components/grid';
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
@ -9,13 +9,12 @@ export function Grid() {
|
|||||||
const [scrollLeft, setScrollLeft] = useState(0);
|
const [scrollLeft, setScrollLeft] = useState(0);
|
||||||
|
|
||||||
const { fields, columnWidth } = useRenderFields();
|
const { fields, columnWidth } = useRenderFields();
|
||||||
const rowOrders = useRowOrdersSelector();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setScrollLeft(0);
|
setScrollLeft(0);
|
||||||
}, [viewId]);
|
}, [viewId]);
|
||||||
|
|
||||||
if (!database || !rowOrders) {
|
if (!database) {
|
||||||
return (
|
return (
|
||||||
<div className={'flex w-full flex-1 flex-col items-center justify-center'}>
|
<div className={'flex w-full flex-1 flex-col items-center justify-center'}>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
@ -24,24 +23,18 @@ export function Grid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowsContext.Provider
|
<div className={'database-grid flex w-full flex-1 flex-col'}>
|
||||||
value={{
|
<GridHeader scrollLeft={scrollLeft} columnWidth={columnWidth} columns={fields} onScrollLeft={setScrollLeft} />
|
||||||
rowOrders,
|
<div className={'grid-scroll-table w-full flex-1'}>
|
||||||
}}
|
<GridTable
|
||||||
>
|
viewId={viewId}
|
||||||
<div className={'database-grid flex w-full flex-1 flex-col'}>
|
scrollLeft={scrollLeft}
|
||||||
<GridHeader scrollLeft={scrollLeft} columnWidth={columnWidth} columns={fields} onScrollLeft={setScrollLeft} />
|
columnWidth={columnWidth}
|
||||||
<div className={'grid-scroll-table w-full flex-1'}>
|
columns={fields}
|
||||||
<GridTable
|
onScrollLeft={setScrollLeft}
|
||||||
viewId={viewId}
|
/>
|
||||||
scrollLeft={scrollLeft}
|
|
||||||
columnWidth={columnWidth}
|
|
||||||
columns={fields}
|
|
||||||
onScrollLeft={setScrollLeft}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</RowsContext.Provider>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,16 @@
|
|||||||
|
const hasLoadedFonts: Set<string> = new Set();
|
||||||
|
|
||||||
export function getFontFamily(attribute: string) {
|
export function getFontFamily(attribute: string) {
|
||||||
return attribute.split('_')[0];
|
const fontFamily = attribute.split('_')[0];
|
||||||
|
|
||||||
|
if (hasLoadedFonts.has(fontFamily)) {
|
||||||
|
return fontFamily;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.WebFont?.load({
|
||||||
|
google: {
|
||||||
|
families: [fontFamily],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return fontFamily;
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,13 @@ export default defineConfig({
|
|||||||
istanbul({
|
istanbul({
|
||||||
cypress: true,
|
cypress: true,
|
||||||
requireEnv: false,
|
requireEnv: false,
|
||||||
|
include: ['src/**/*'],
|
||||||
|
exclude: [
|
||||||
|
'**/__tests__/**/*',
|
||||||
|
'cypress/**/*',
|
||||||
|
'node_modules/**/*',
|
||||||
|
'src/application/services/tauri-services/**/*',
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
usePluginImport({
|
usePluginImport({
|
||||||
libraryName: '@mui/icons-material',
|
libraryName: '@mui/icons-material',
|
||||||
@ -130,6 +137,12 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: ['react', 'react-dom', '@mui/icons-material/ErrorOutline', '@mui/icons-material/CheckCircleOutline'],
|
include: [
|
||||||
|
'react',
|
||||||
|
'react-dom',
|
||||||
|
'@mui/icons-material/ErrorOutline',
|
||||||
|
'@mui/icons-material/CheckCircleOutline',
|
||||||
|
'@mui/icons-material/FunctionsOutlined',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user