mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-16 14:40:43 +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"
|
||||
- "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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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/',
|
||||
],
|
||||
};
|
@ -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
@ -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> {
|
||||
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> {
|
||||
|
@ -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();
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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 },
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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 {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
@ -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 { 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,
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>) {
|
||||
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;
|
||||
|
@ -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);
|
||||
|
@ -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:
|
||||
|
@ -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 { 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);
|
||||
}
|
@ -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,
|
||||
} 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 [];
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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>) {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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 }) {
|
||||
|
@ -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>) {
|
||||
|
@ -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>) {
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user