feat: improve test coverage (#5481)

This commit is contained in:
Kilu.He 2024-06-06 17:48:58 +08:00 committed by GitHub
parent d73e388d01
commit 3b72f90ca5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 2274 additions and 716 deletions

View File

@ -6,6 +6,7 @@ on:
- ".github/workflows/web2_ci.yaml"
- "frontend/appflowy_web_app/**"
- "frontend/resources/**"
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
@ -52,8 +53,13 @@ jobs:
run: |
pnpm run test:unit
- name: Generate and post coverage summary
working-directory: frontend/appflowy_web_app
run: |
pnpm run merge-coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
with:
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

View File

@ -1,6 +1,6 @@
{
"all": true,
"extends": "@istanbuljs/nyc-config-typescript",
"extends": "@istanbuljs/nyc-config-babel",
"include": [
"src/**/*.ts",
"src/**/*.tsx"
@ -15,7 +15,8 @@
"text",
"html",
"text-summary",
"json"
"json",
"lcov"
],
"temp-dir": "coverage/.nyc_output",
"report-dir": "coverage/cypress"

View File

@ -5,7 +5,7 @@ const esModules = ['lodash-es', 'nanoid'].join('|');
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testEnvironment: 'jsdom',
roots: ['<rootDir>'],
modulePaths: [compilerOptions.baseUrl],
moduleNameMapper: {
@ -14,10 +14,28 @@ module.exports = {
'^nanoid(/(.*)|$)': 'nanoid$1',
},
'transform': {
'^.+\\.(j|t)sx?$': 'ts-jest',
'(.*)/node_modules/nanoid/.+\\.(j|t)sx?$': 'ts-jest',
},
'transformIgnorePatterns': [`/node_modules/(?!${esModules})`],
testMatch: ['**/*.test.ts'],
testMatch: ['**/*.test.ts', '**/*.test.tsx'],
coverageDirectory: '<rootDir>/coverage/jest',
collectCoverage: true,
coverageProvider: 'v8',
coveragePathIgnorePatterns: [
'/cypress/',
'/coverage/',
'/node_modules/',
'/__tests__/',
'/__mocks__/',
'/__fixtures__/',
'/__helpers__/',
'/__utils__/',
'/__constants__/',
'/__types__/',
'/__mocks__/',
'/__stubs__/',
'/__fixtures__/',
'/application/folder-yjs/',
],
};

View File

@ -21,8 +21,7 @@
"test:components": "cypress run --component --browser chrome --headless",
"test:unit": "jest --coverage",
"test:cy": "cypress run",
"merge-coverage": "node scripts/merge-coverage.cjs",
"coverage": "pnpm run test:unit && pnpm run test:components && pnpm run merge-coverage"
"coverage": "pnpm run test:unit && pnpm run test:components"
},
"dependencies": {
"@appflowyinc/client-api-wasm": "0.0.3",
@ -99,11 +98,15 @@
"yjs": "^13.6.14"
},
"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",
"@istanbuljs/nyc-config-babel": "^3.0.0",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@svgr/plugin-svgo": "^8.0.1",
"@tauri-apps/cli": "^1.5.11",
"@testing-library/react": "^16.0.0",
"@types/google-protobuf": "^3.15.12",
"@types/is-hotkey": "^0.1.7",
"@types/jest": "^29.5.3",

File diff suppressed because it is too large Load Diff

View File

@ -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`);
}

View File

@ -369,7 +369,19 @@ export interface YDocument 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> {

View File

@ -27,6 +27,7 @@ import {
filterBy,
} from '../filter';
import { expect } from '@jest/globals';
import * as Y from 'yjs';
describe('Text filter check', () => {
const text = 'Hello, world!';
@ -540,6 +541,15 @@ describe('Database filterBy', () => {
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', () => {
const { filters, fields, rowMap } = withTestingData();
const filter = withRichTextFilter();

View File

@ -78,5 +78,25 @@
"field_id": "url_field",
"condition": "desc",
"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"
}
}

View File

@ -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 { withTestingRows } from '@/application/database-yjs/__tests__/withTestingRows';
import { expect } from '@jest/globals';
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', () => {
let rows: Row[];
@ -95,4 +104,69 @@ describe('Database group', () => {
]);
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 },
],
],
])
);
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -4,7 +4,9 @@ import { withTestingRows } from '@/application/database-yjs/__tests__/withTestin
import {
withCheckboxSort,
withChecklistSort,
withCreatedAtSort,
withDateTimeSort,
withLastModifiedSort,
withMultiSelectOptionSort,
withNumberSort,
withRichTextSort,
@ -19,10 +21,12 @@ import {
withSelectOptionTestingField,
withURLTestingField,
withChecklistTestingField,
withRelationTestingField,
} from './withTestingField';
import { sortBy, parseCellDataForSort } from '../sort';
import * as Y from 'yjs';
import { expect } from '@jest/globals';
import { YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
describe('parseCellDataForSort', () => {
it('should parse data correctly based on field type', () => {
@ -127,6 +131,17 @@ describe('parseCellDataForSort', () => {
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', () => {
@ -136,6 +151,53 @@ describe('Database sortBy', () => {
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', () => {
const { sorts, fields, rowMap } = withTestingData();
const sort = withNumberSort();
@ -311,4 +373,25 @@ describe('Database sortBy', () => {
.join(',');
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');
});
});

View File

@ -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;
}

View File

@ -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 { withTestingRowDataMap } from '@/application/database-yjs/__tests__/withTestingRows';
import {
withTestingRowData,
withTestingRowDataMap,
withTestingRows,
} from '@/application/database-yjs/__tests__/withTestingRows';
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() {
const doc = new Y.Doc();
@ -27,5 +49,133 @@ export function withTestingData() {
rowMap,
sorts,
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,
};
}

View File

@ -4,7 +4,8 @@ import {
YjsDatabaseKey,
YMapFieldTypeOption,
} 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';
export function withTestingFields() {
@ -39,6 +40,14 @@ export function withTestingFields() {
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;
}
@ -56,13 +65,31 @@ export function withRichTextTestingField() {
export function withNumberTestingField() {
const field = new Y.Map() as YDatabaseField;
const now = Date.now().toString();
field.set(YjsDatabaseKey.name, 'Number Field');
field.set(YjsDatabaseKey.id, 'number_field');
field.set(YjsDatabaseKey.type, String(FieldType.Number));
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.type_option, typeOption);
return field;
}
@ -151,3 +178,27 @@ export function withChecklistTestingField() {
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;
}

View File

@ -39,6 +39,8 @@ export function withTestingRowData(id: string, index: number) {
rowData.set(YjsDatabaseKey.id, id);
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;

View File

@ -89,3 +89,25 @@ export function withChecklistSort(isAscending: boolean = true) {
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;
}

View File

@ -1,5 +1,5 @@
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 React from 'react';
import { YArray } from 'yjs/dist/src/types/YArray';

View File

@ -18,7 +18,15 @@ export const getCellData = (rowId: string, fieldId: string, rowMetas: Y.Map<YDoc
};
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();
};
export const generateUUID = () => uuidv5(Date.now().toString(), uuidv5.URL);

View File

@ -1,5 +1,4 @@
import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
import { Row } from '@/application/database-yjs/selector';
import { createContext, useContext } from 'react';
import * as Y from 'yjs';
@ -72,17 +71,3 @@ export function useDatabaseFields() {
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;
}

View File

@ -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');
});
});

View File

@ -78,6 +78,9 @@ function createPredicate(conditions: ((row: Row) => boolean)[]) {
export function filterBy(rows: Row[], filters: YDatabaseFilters, fields: YDatabaseFields, rowMetas: Y.Map<YDoc>) {
const filterArray = filters.toArray();
if (filterArray.length === 0 || rowMetas.size === 0 || fields.size === 0) return rows;
const conditions = filterArray.map((filter) => {
return (row: { id: string }) => {
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) {
if (isNaN(Number(data)) || isNaN(Number(content)) || data === '' || content === '') {
if (condition === NumberFilterCondition.NumberIsEmpty && data === '') {
return true;
if (condition === NumberFilterCondition.NumberIsEmpty) {
return data === '';
}
if (condition === NumberFilterCondition.NumberIsNotEmpty && data !== '') {
return true;
if (condition === NumberFilterCondition.NumberIsNotEmpty) {
return data !== '';
}
return false;
@ -169,10 +172,6 @@ export function numberFilterCheck(data: string, content: string, condition: numb
return decimal < filterDecimal;
case NumberFilterCondition.LessThanOrEqualTo:
return decimal <= filterDecimal;
case NumberFilterCondition.NumberIsEmpty:
return data === '';
case NumberFilterCondition.NumberIsNotEmpty:
return data !== '';
default:
return false;
}
@ -228,14 +227,6 @@ export function selectOptionFilterCheck(data: string, content: string, condition
case SelectOptionFilterCondition.OptionDoesNotContain:
return some(filterOptionIds, (option) => !selectedOptionIds.includes(option));
// Ensure selectedOptionIds is empty
case SelectOptionFilterCondition.OptionIsEmpty:
return selectedOptionIds.length === 0;
// Ensure selectedOptionIds is not empty
case SelectOptionFilterCondition.OptionIsNotEmpty:
return selectedOptionIds.length !== 0;
// Default case, if no conditions match
default:
return false;

View File

@ -14,18 +14,17 @@ import {
useDatabaseView,
useIsDatabaseRowPage,
useRowDocMap,
useRows,
useViewId,
} from '@/application/database-yjs/context';
import { filterBy, parseFilter } from '@/application/database-yjs/filter';
import { groupByField } from '@/application/database-yjs/group';
import { sortBy } from '@/application/database-yjs/sort';
import { useViewsIdSelector } from '@/application/folder-yjs';
import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
import { DateTimeCell } from '@/components/database/components/cell/cell.type';
import dayjs from 'dayjs';
import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse';
import { DateTimeCell } from '@/application/database-yjs/cell.type';
import * as dayjs from 'dayjs';
import { throttle } from 'lodash-es';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import Y from 'yjs';
import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMetaKey, SortCondition } from './database.type';
@ -149,12 +148,6 @@ export function useFieldsSelector(visibilitys: FieldVisibility[] = defaultVisibl
return columns;
}
export function useRowsSelector() {
const rowOrders = useRows();
return useMemo(() => rowOrders ?? [], [rowOrders]);
}
export function useFieldSelector(fieldId: string) {
const database = useDatabase();
const [field, setField] = useState<YDatabaseField | null>(null);
@ -403,7 +396,7 @@ export function useRowsByGroup(groupId: string) {
if (!fieldId || !rowOrders || !rows) return;
const onConditionsChange = () => {
if (rows.size !== rowOrders?.length) return;
if (rows.size < rowOrders?.length) return;
const newResult = new Map<string, Row[]>();
@ -456,7 +449,7 @@ export function useRowOrdersSelector() {
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) {
setRowOrders(originalRowOrders);
return;
@ -691,7 +684,7 @@ export function useCalendarLayoutSetting() {
export function usePrimaryFieldId() {
const database = useDatabase();
const [primaryFieldId, setPrimaryFieldId] = React.useState<string | null>(null);
const [primaryFieldId, setPrimaryFieldId] = useState<string | null>(null);
useEffect(() => {
const fields = database?.get(YjsDatabaseKey.fields);

View File

@ -15,6 +15,8 @@ import * as Y from 'yjs';
export function sortBy(rows: Row[], sorts: YDatabaseSorts, fields: YDatabaseFields, rowMetas: Y.Map<YDoc>) {
const sortArray = sorts.toArray();
if (sortArray.length === 0 || rowMetas.size === 0 || fields.size === 0) return rows;
const iteratees = sortArray.map((sort) => {
return (row: { id: string }) => {
const fieldId = sort.get(YjsDatabaseKey.field_id);
@ -26,8 +28,7 @@ export function sortBy(rows: Row[], sorts: YDatabaseSorts, fields: YDatabaseFiel
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 (fieldType === FieldType.LastEditedTime) {
@ -69,9 +70,9 @@ export function parseCellDataForSort(field: YDatabaseField, data: string | boole
return data === 'Yes';
case FieldType.SingleSelect:
case FieldType.MultiSelect:
return parseSelectOptionCellData(field, typeof data === 'string' ? data : '');
return parseSelectOptionCellData(field, data as string);
case FieldType.Checklist:
return parseChecklistData(typeof data === 'string' ? data : '')?.percentage ?? 0;
return parseChecklistData(data as string)?.percentage ?? 0;
case FieldType.DateTime:
return Number(data);
case FieldType.Relation:

View File

@ -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);
}

View File

@ -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: '@' }],
},
]);
});
});

View File

@ -1,5 +1,5 @@
import { withTestingYDoc, withTestingYjsEditor } from './withTestingYjsEditor';
import { yDocToSlateContent } from '../convert';
import { yDocToSlateContent } from '../utils/convert';
import { createEditor, Editor } from 'slate';
import { expect } from '@jest/globals';
import * as Y from 'yjs';
@ -39,3 +39,34 @@ export async function runCollaborationTest() {
expect(yjsEditor.children).toEqual(remote.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);
}

View File

@ -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([]);
});
});

View File

@ -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,
});
},
};
}

View File

@ -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);
}

View File

@ -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();
});
});

View File

@ -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;
}

View File

@ -11,21 +11,19 @@ import {
BlockType,
} from '@/application/collab.type';
import { BlockJson } from '@/application/slate-yjs/utils/types';
import { getFontFamily } from '@/utils/font';
import { uniq } from 'lodash-es';
import { Element, Text } from 'slate';
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;
const fontFamilys: string[] = [];
export function yDataToSlateContent({
blocks,
rootId,
childrenMap,
textMap,
}: {
blocks: YBlocks;
childrenMap: YChildrenMap;
textMap: YTextMap;
rootId: string;
}): Element | undefined {
function traverse(id: string) {
const block = blocks.get(id).toJSON() as BlockJson;
const childrenId = block.children as string;
@ -44,7 +42,9 @@ export function yDocToSlateContent(doc: YDoc): Element | undefined {
let delta;
if (!textId) {
const yText = textId ? textMap.get(textId) : undefined;
if (!yText) {
if (children.length === 0) {
children.push({
text: '',
@ -64,18 +64,12 @@ export function yDocToSlateContent(doc: YDoc): Element | undefined {
}
}
} else {
delta = textMap.get(textId)?.toDelta();
delta = yText.toDelta();
}
try {
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 = {
textId,
type: YjsEditorKey.text,
@ -85,30 +79,39 @@ export function yDocToSlateContent(doc: YDoc): Element | undefined {
children.unshift(textNode);
return slateNode;
} catch (e) {
console.error(e);
return;
}
}
const root = blocks.get(pageId);
const root = blocks.get(rootId);
if (!root) return;
const result = traverse(pageId);
const result = traverse(rootId);
if (!result) return;
if (fontFamilys.length > 0) {
window.WebFont?.load({
google: {
families: uniq(fontFamilys),
},
});
}
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 {
const data = block.data;
let blockData;
@ -116,7 +119,7 @@ export function blockToSlateNode(block: BlockJson): Element {
try {
blockData = data ? JSON.parse(data) : {};
} catch (e) {
blockData = {};
// do nothing
}
return {
@ -128,13 +131,12 @@ export function blockToSlateNode(block: BlockJson): Element {
};
}
export function deltaInsertToSlateNode({
attributes,
insert,
}: {
export interface YDelta {
insert: string;
attributes: Record<string, string | number | undefined | boolean>;
}): Element | Text | Element[] {
attributes?: Record<string, string | number | undefined | boolean>;
}
export function deltaInsertToSlateNode({ attributes, insert }: YDelta): Element | Text | Element[] {
const matchInlines = transformToInlineElement({
insert,
attributes,
@ -145,17 +147,7 @@ export function deltaInsertToSlateNode({
}
if (attributes) {
if ('font_color' in attributes && attributes['font_color'] === '') {
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'];
}
dealWithEmptyAttribute(attributes);
}
return {
@ -164,10 +156,15 @@ export function deltaInsertToSlateNode({
};
}
export function transformToInlineElement(op: {
insert: string;
attributes: Record<string, string | number | undefined | boolean>;
}): Element[] {
function dealWithEmptyAttribute(attributes: Record<string, string | number | undefined | boolean>) {
for (const key in attributes) {
if (!attributes[key]) {
delete attributes[key];
}
}
}
export function transformToInlineElement(op: YDelta): Element[] {
const attributes = op.attributes;
if (!attributes) return [];

View File

@ -1,5 +1,5 @@
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 React from 'react';

View File

@ -10,7 +10,7 @@ import { CheckboxCell } from '@/components/database/components/cell/checkbox';
import { SelectOptionCell } from '@/components/database/components/cell/select-option';
import { DateTimeCell } from '@/components/database/components/cell/date';
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';
export function Cell(props: CellProps<CellType>) {

View File

@ -11,15 +11,3 @@ export const SelectOptionColorMap = {
[SelectOptionColor.Aqua]: '--tint-aqua',
[SelectOptionColor.Blue]: '--tint-blue',
};
export const SelectOptionColorTextMap = {
[SelectOptionColor.Purple]: 'purpleColor',
[SelectOptionColor.Pink]: 'pinkColor',
[SelectOptionColor.LightPink]: 'lightPinkColor',
[SelectOptionColor.Orange]: 'orangeColor',
[SelectOptionColor.Yellow]: 'yellowColor',
[SelectOptionColor.Lime]: 'limeColor',
[SelectOptionColor.Green]: 'greenColor',
[SelectOptionColor.Aqua]: 'aquaColor',
[SelectOptionColor.Blue]: 'blueColor',
} as const;

View File

@ -1,7 +1,7 @@
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';
import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg';
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>) {
const checked = cell?.data;

View File

@ -1,5 +1,5 @@
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 React, { useMemo } from 'react';

View File

@ -1,6 +1,6 @@
import { FieldType } from '@/application/database-yjs';
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 { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg';

View File

@ -5,7 +5,7 @@ import {
parseNumberTypeOptions,
FieldType,
} 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 Decimal from 'decimal.js';

View File

@ -1,5 +1,5 @@
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 OpenAction from '@/components/database/components/database-row/OpenAction';
import { getPlatform } from '@/utils/platform';

View File

@ -1,5 +1,5 @@
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 React from 'react';

View File

@ -6,7 +6,7 @@ import {
useFieldSelector,
useNavigateToRow,
} 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 { useGetDatabaseDispatch } from '@/components/database/Database.hooks';
import React, { useEffect, useMemo, useState } from 'react';

View File

@ -1,5 +1,5 @@
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';
export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) {

View File

@ -1,7 +1,7 @@
import { useFieldSelector, parseSelectOptionTypeOptions } from '@/application/database-yjs';
import { Tag } from '@/components/_shared/tag';
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';
export function SelectOptionCell({ cell, fieldId, style, placeholder }: CellProps<SelectOptionCellType>) {

View File

@ -1,5 +1,5 @@
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';
export function TextCell({ cell, style, placeholder }: CellProps<TextCellType>) {

View File

@ -1,5 +1,5 @@
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 React, { useMemo } from 'react';

View File

@ -2,7 +2,7 @@ import { FieldId, YjsDatabaseKey } from '@/application/collab.type';
import { useCellSelector } from '@/application/database-yjs';
import { useFieldSelector } from '@/application/database-yjs/selector';
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 React, { useEffect, useMemo, useRef } from 'react';

View File

@ -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';
@ -15,16 +15,19 @@ export type RenderRow = {
};
export function useRenderRows() {
const rows = useRowsSelector();
const rows = useRowOrdersSelector();
const readOnly = useReadOnly();
const renderRows = useMemo(() => {
return [
...rows.map((row) => ({
const rowItems =
rows?.map((row) => ({
type: RenderRowType.Row,
rowId: row.id,
height: row.height,
})),
})) ?? [];
return [
...rowItems,
!readOnly && {
type: RenderRowType.NewRow,

View File

@ -1,6 +1,6 @@
import { YjsDatabaseKey } from '@/application/collab.type';
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 { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified';
import { DateTimeCell } from '@/components/database/components/cell/date';

View File

@ -1,5 +1,5 @@
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 React, { useMemo } from 'react';
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';

View File

@ -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 React from 'react';

View File

@ -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 { CircularProgress } from '@mui/material';
import React, { useEffect, useState } from 'react';
@ -9,13 +9,12 @@ export function Grid() {
const [scrollLeft, setScrollLeft] = useState(0);
const { fields, columnWidth } = useRenderFields();
const rowOrders = useRowOrdersSelector();
useEffect(() => {
setScrollLeft(0);
}, [viewId]);
if (!database || !rowOrders) {
if (!database) {
return (
<div className={'flex w-full flex-1 flex-col items-center justify-center'}>
<CircularProgress />
@ -24,24 +23,18 @@ export function Grid() {
}
return (
<RowsContext.Provider
value={{
rowOrders,
}}
>
<div className={'database-grid flex w-full flex-1 flex-col'}>
<GridHeader scrollLeft={scrollLeft} columnWidth={columnWidth} columns={fields} onScrollLeft={setScrollLeft} />
<div className={'grid-scroll-table w-full flex-1'}>
<GridTable
viewId={viewId}
scrollLeft={scrollLeft}
columnWidth={columnWidth}
columns={fields}
onScrollLeft={setScrollLeft}
/>
</div>
<div className={'database-grid flex w-full flex-1 flex-col'}>
<GridHeader scrollLeft={scrollLeft} columnWidth={columnWidth} columns={fields} onScrollLeft={setScrollLeft} />
<div className={'grid-scroll-table w-full flex-1'}>
<GridTable
viewId={viewId}
scrollLeft={scrollLeft}
columnWidth={columnWidth}
columns={fields}
onScrollLeft={setScrollLeft}
/>
</div>
</RowsContext.Provider>
</div>
);
}

View File

@ -1,3 +1,16 @@
const hasLoadedFonts: Set<string> = new Set();
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;
}

View File

@ -44,6 +44,13 @@ export default defineConfig({
istanbul({
cypress: true,
requireEnv: false,
include: ['src/**/*'],
exclude: [
'**/__tests__/**/*',
'cypress/**/*',
'node_modules/**/*',
'src/application/services/tauri-services/**/*',
],
}),
usePluginImport({
libraryName: '@mui/icons-material',
@ -130,6 +137,12 @@ export default defineConfig({
},
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',
],
},
});