@pcg/dynamic-components 1.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/index.d.ts +1816 -0
- package/dist/index.js +1564 -0
- package/dist/index.js.map +1 -0
- package/eslint.config.cjs +14 -0
- package/package.json +30 -0
- package/src/assertions/basic.ts +58 -0
- package/src/assertions/containers.ts +76 -0
- package/src/assertions/index.ts +6 -0
- package/src/assertions/paths.ts +12 -0
- package/src/assertions/rich-text.ts +16 -0
- package/src/assertions/yjs.ts +25 -0
- package/src/data-objects/data-object.ts +34 -0
- package/src/data-objects/index.ts +3 -0
- package/src/data-objects/rich-text.ts +38 -0
- package/src/dynamic-components/fractional-indexing.ts +321 -0
- package/src/dynamic-components/index.ts +6 -0
- package/src/dynamic-components/paths.ts +194 -0
- package/src/dynamic-components/registry/chats.ts +24 -0
- package/src/dynamic-components/registry/content.ts +118 -0
- package/src/dynamic-components/registry/forms.ts +525 -0
- package/src/dynamic-components/registry/index.ts +6 -0
- package/src/dynamic-components/registry/layout.ts +86 -0
- package/src/dynamic-components/registry/uikit-dynamic-component.ts +84 -0
- package/src/dynamic-components/tools.ts +195 -0
- package/src/dynamic-components/types.ts +237 -0
- package/src/index.ts +7 -0
- package/src/paths/array-keys.ts +164 -0
- package/src/paths/array-ops.ts +124 -0
- package/src/paths/basic-ops.ts +181 -0
- package/src/paths/constants.ts +1 -0
- package/src/paths/index.ts +7 -0
- package/src/paths/tools.ts +42 -0
- package/src/paths/types.ts +133 -0
- package/src/y-components/index.ts +3 -0
- package/src/y-components/tools.ts +234 -0
- package/src/y-components/types.ts +19 -0
- package/src/y-tools/array-path-ops.ts +240 -0
- package/src/y-tools/basic-path-ops.ts +189 -0
- package/src/y-tools/index.ts +6 -0
- package/src/y-tools/tools.ts +122 -0
- package/src/y-tools/types.ts +32 -0
- package/src/y-tools/y-array-keys.ts +47 -0
- package/tests/assertions/basic-types.test.ts +78 -0
- package/tests/assertions/containers.test.ts +72 -0
- package/tests/assertions/paths.test.ts +23 -0
- package/tests/assertions/yjs.test.ts +33 -0
- package/tests/dynamic-components/paths.test.ts +171 -0
- package/tests/dynamic-components/tools.test.ts +121 -0
- package/tests/paths/array-keys.test.ts +182 -0
- package/tests/paths/array-ops.test.ts +164 -0
- package/tests/paths/basic-ops.test.ts +263 -0
- package/tests/paths/tools.test.ts +55 -0
- package/tests/y-components/tools.test.ts +198 -0
- package/tests/y-tools/array-base-ops.test.ts +55 -0
- package/tests/y-tools/array-path-ops.test.ts +95 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +13 -0
- package/tsdown.config.ts +18 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
|
|
3
|
+
import { isObject } from '@/assertions/index.js';
|
|
4
|
+
import { sortByPosition } from '@/paths/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the index of the first item in a Y.Array that matches a condition.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* let yArray = new Y.Array();
|
|
11
|
+
* yArray.push(['item1', 'item2', 'item3']);
|
|
12
|
+
* const index = findIndexInYArray(yArray, (item) => item === 'item2');
|
|
13
|
+
* // index is now: 1
|
|
14
|
+
*/
|
|
15
|
+
export const findIndexInYArray = <T = unknown>(yArray: Y.Array<T>, isMatches: (item: T) => boolean): number => {
|
|
16
|
+
return yArray.toArray().findIndex(isMatches);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns the first item in a Y.Array that matches a condition.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* let yArray = new Y.Array();
|
|
24
|
+
* yArray.push(['item1', 'item2', 'item3']);
|
|
25
|
+
* const item = findInYArray(yArray, (item) => item === 'item2');
|
|
26
|
+
* // item is now: 'item2'
|
|
27
|
+
*/
|
|
28
|
+
export const findInYArray = <T = unknown>(yArray: Y.Array<T>, isMatches: (item: T) => boolean): T | null => {
|
|
29
|
+
const index = findIndexInYArray(yArray, isMatches);
|
|
30
|
+
if (index === -1) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return yArray.get(index);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Converts an object to a Y.Map.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* const object = { key: 'value' };
|
|
42
|
+
* const yMap = objectToYMap(object);
|
|
43
|
+
* // yMap now looks like: { key: 'value' }
|
|
44
|
+
*/
|
|
45
|
+
export const objectToYMap = (values: object) => {
|
|
46
|
+
const entries: [string, unknown][] = Object.entries(values).map(([key, value]) => {
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
return [key, arrayToYArray(value)];
|
|
49
|
+
} else if (isObject(value)) {
|
|
50
|
+
return [key, objectToYMap(value)];
|
|
51
|
+
} else {
|
|
52
|
+
return [key, value];
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return new Y.Map(entries);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Converts an array to a Y.Array.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* const array = ['item1', 'item2', 'item3'];
|
|
64
|
+
* const yArray = arrayToYArray(array);
|
|
65
|
+
* // yArray now looks like: ['item1', 'item2', 'item3']
|
|
66
|
+
*/
|
|
67
|
+
export const arrayToYArray = (array: unknown[], yArray: Y.Array<unknown> = new Y.Array()) => {
|
|
68
|
+
const items = array.map((it) => isObject(it) ? objectToYMap(it) : it);
|
|
69
|
+
yArray.insert(0, items);
|
|
70
|
+
|
|
71
|
+
return yArray;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns a sorted Y.Array based on the position of each Y.Map in it.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* let yArray = new Y.Array();
|
|
79
|
+
* let yMap1 = new Y.Map();
|
|
80
|
+
* let yMap2 = new Y.Map();
|
|
81
|
+
* yMap1.set('position', 'a0');
|
|
82
|
+
* yMap2.set('position', 'b0');
|
|
83
|
+
* yArray.push([yMap2, yMap1]);
|
|
84
|
+
* const sortedYArray = getSortedYArray(yArray);
|
|
85
|
+
* // sortedYArray now looks like: [ yMap1, yMap2 ]
|
|
86
|
+
*/
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
+
export const getSortedYArray = <T extends Y.Map<any>>(yArray: Y.Array<T>) => yArray
|
|
89
|
+
.toArray()
|
|
90
|
+
.sort((a, b) => sortByPosition((a.get('position') as string), (b.get('position') as string)));
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Returns references to a Y.Map and its neighbors in a Y.Array based on its position.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* let yArray = new Y.Array();
|
|
97
|
+
* let yMap1 = new Y.Map();
|
|
98
|
+
* let yMap2 = new Y.Map();
|
|
99
|
+
* yMap1.set('position', 'a0');
|
|
100
|
+
* yMap2.set('position', 'b0');
|
|
101
|
+
* yArray.push([yMap2, yMap1]);
|
|
102
|
+
* const refs = getYArrayRefsByPosition(yArray, 'a0');
|
|
103
|
+
* // refs now looks like: { index: 0, ref: yMap1, right: yMap2, left: undefined, sortedArray: [yMap1, yMap2] }
|
|
104
|
+
*/
|
|
105
|
+
export const getYArrayRefsByPosition = <T extends Y.Map<unknown>>(yArray: Y.Array<T>, position: string) => {
|
|
106
|
+
const sortedArray = getSortedYArray(yArray);
|
|
107
|
+
// const currentYArrayIndex = sortedArray.findIndex((it) => it === yComponent);
|
|
108
|
+
const index = sortedArray.findIndex((it) => it.get('position') === position);
|
|
109
|
+
if (index === -1) {
|
|
110
|
+
console.error('sortedArray', sortedArray);
|
|
111
|
+
throw new Error(`Relative item with position ${position} not found`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
index,
|
|
116
|
+
ref: sortedArray[index],
|
|
117
|
+
right: sortedArray[index + 1],
|
|
118
|
+
left: sortedArray[index - 1],
|
|
119
|
+
sortedArray,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents changes in Y.js awareness state. Contains arrays of added, removed, and updated items.
|
|
3
|
+
*/
|
|
4
|
+
export interface AwarenessStateChange {
|
|
5
|
+
added: number[];
|
|
6
|
+
removed: number[];
|
|
7
|
+
updated: number[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Represents the awareness state. This includes a cursor, user information (id, name, avatarUrl), and a color.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const awarenessState: AwarenessState = {
|
|
15
|
+
* cursor: ['xxx', 'yyy'], // rpath
|
|
16
|
+
* user: {
|
|
17
|
+
* id: 'abc123',
|
|
18
|
+
* name: 'John Doe',
|
|
19
|
+
* avatarUrl: 'https://example.com/avatar.jpg'
|
|
20
|
+
* },
|
|
21
|
+
* color: '#ff0000'
|
|
22
|
+
* };
|
|
23
|
+
*/
|
|
24
|
+
export interface AwarenessState {
|
|
25
|
+
cursor: string[];
|
|
26
|
+
user: {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
avatarUrl?: string;
|
|
30
|
+
};
|
|
31
|
+
color: string;
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
|
|
3
|
+
import { isYArray, isYMap } from '@/assertions/index.js';
|
|
4
|
+
import {
|
|
5
|
+
extractIdFromArrayKey, extractPositionFromArrayKey, isIdArrayKey, isPositionArrayKey,
|
|
6
|
+
} from '@/paths/index.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns the index of an item in a Y.Array based on its key.
|
|
10
|
+
* The key can represent either the id or the position of the item in the array.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* let yArray = new Y.Array();
|
|
14
|
+
* let yMap = new Y.Map();
|
|
15
|
+
* yMap.set('id', '123');
|
|
16
|
+
* yMap.set('position', 'a0');
|
|
17
|
+
* yArray.push([yMap]);
|
|
18
|
+
* const index = getYArrayIndexByArrayKey(yArray, 'id:123');
|
|
19
|
+
* const index = getYArrayIndexByArrayKey(yArray, 'pos:a0');
|
|
20
|
+
* // index is now: 0
|
|
21
|
+
*/
|
|
22
|
+
export const getYArrayIndexByArrayKey = (yArray: unknown, key: string): number => {
|
|
23
|
+
if (!isYArray<Y.Map<unknown>>(yArray)) {
|
|
24
|
+
console.error(`Non-YArray type`, yArray);
|
|
25
|
+
throw new Error(`Can't use array key in non YArray type (${key})`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (isIdArrayKey(key)) {
|
|
29
|
+
const id = extractIdFromArrayKey(key);
|
|
30
|
+
const index = yArray.toArray().findIndex((it) => isYMap(it) && it.get('id') === id);
|
|
31
|
+
if (index === -1) {
|
|
32
|
+
throw new Error(`Can't find index by id ${id} (${key})`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return index;
|
|
36
|
+
} else if (isPositionArrayKey(key)) {
|
|
37
|
+
const position = extractPositionFromArrayKey(key);
|
|
38
|
+
const index = yArray.toArray().findIndex((it) => isYMap(it) && it.get('position') === position);
|
|
39
|
+
if (index === -1) {
|
|
40
|
+
throw new Error(`Can't find index by position ${position} (${key})`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return index;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new Error(`Unknown array key type (${key})`);
|
|
47
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe, expect, test,
|
|
3
|
+
} from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
isCallable,
|
|
6
|
+
isIndex,
|
|
7
|
+
isNullOrUndefined,
|
|
8
|
+
isNumber,
|
|
9
|
+
isString,
|
|
10
|
+
} from '../../src/assertions/index.js';
|
|
11
|
+
|
|
12
|
+
const object = {
|
|
13
|
+
a: 2,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('isCallable', () => {
|
|
17
|
+
test('must return false on non-callable', () => {
|
|
18
|
+
expect(isCallable('123')).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('must return true on callable', () => {
|
|
22
|
+
expect(isCallable(() => 0)).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('isIndex', () => {
|
|
27
|
+
test.each(['string', -1, '-1', [], object, null, undefined, () => 0])('must return false on non-index value: %p', (value) => {
|
|
28
|
+
expect(isIndex(value)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test.each([
|
|
32
|
+
0,
|
|
33
|
+
1,
|
|
34
|
+
'1',
|
|
35
|
+
])('must return true on index value: %p', (value) => {
|
|
36
|
+
expect(isIndex(value)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('isString', () => {
|
|
41
|
+
test.each([1, [], object, null, undefined, () => 0])('must return false on non-string value: %p', (value) => {
|
|
42
|
+
expect(isString(value)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test.each([
|
|
46
|
+
'',
|
|
47
|
+
'text',
|
|
48
|
+
])('must return true on string value: %p', (value) => {
|
|
49
|
+
expect(isString(value)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('isNumber', () => {
|
|
54
|
+
test.each(['text', [], object, null, undefined, () => 0])('must return false on non-number value: %p', (value) => {
|
|
55
|
+
expect(isNumber(value)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test.each([
|
|
59
|
+
0,
|
|
60
|
+
-1,
|
|
61
|
+
100,
|
|
62
|
+
])('must return true on number value: %p', (value) => {
|
|
63
|
+
expect(isNumber(value)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('isNullOrUndefined', () => {
|
|
68
|
+
test.each(['', [], object, false, 0, () => 0])('must return false on non-null and non-undefined value: %p', (value) => {
|
|
69
|
+
expect(isNullOrUndefined(value)).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test.each([
|
|
73
|
+
null,
|
|
74
|
+
[].find((it) => it === 2),
|
|
75
|
+
])('must return true on null or undefined value: %p', (value) => {
|
|
76
|
+
expect(isNullOrUndefined(value)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe, expect, test,
|
|
3
|
+
} from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
isContainerValue,
|
|
6
|
+
isEmptyArray,
|
|
7
|
+
isEmptyContainer,
|
|
8
|
+
isObject,
|
|
9
|
+
} from '../../src/assertions/index.js';
|
|
10
|
+
|
|
11
|
+
describe('isObject', () => {
|
|
12
|
+
test.each(['string', 4, [1, 2, 3], null, undefined, () => 0])('must return false on non object types: %p', (value) => {
|
|
13
|
+
expect(isObject(value)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test.each([
|
|
17
|
+
{
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
a: 1,
|
|
21
|
+
},
|
|
22
|
+
])('must return true on object types', (value) => {
|
|
23
|
+
expect(isObject(value)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('isEmptyArray', () => {
|
|
28
|
+
test('must return false on non-empty array', () => {
|
|
29
|
+
expect(isEmptyArray([1, 2, 3])).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('must return true on empty array', () => {
|
|
33
|
+
expect(isEmptyArray([])).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('isContainerValue', () => {
|
|
38
|
+
test.each(['string', 4, null, undefined, () => 0])('must return false on non-container value: %p', (value) => {
|
|
39
|
+
expect(isContainerValue(value)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test.each([
|
|
43
|
+
{
|
|
44
|
+
},
|
|
45
|
+
[],
|
|
46
|
+
[1, 2, 3],
|
|
47
|
+
{
|
|
48
|
+
a: 2,
|
|
49
|
+
},
|
|
50
|
+
])('must return true on container value: %p', (value) => {
|
|
51
|
+
expect(isContainerValue(value)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('isEmptyContainer', () => {
|
|
56
|
+
test.each([
|
|
57
|
+
[1],
|
|
58
|
+
{
|
|
59
|
+
n: 1,
|
|
60
|
+
},
|
|
61
|
+
])('must return false on non-empty container value: %p', (value) => {
|
|
62
|
+
expect(isEmptyContainer(value)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test.each([
|
|
66
|
+
[],
|
|
67
|
+
{
|
|
68
|
+
},
|
|
69
|
+
])('must return true on empty container value: %p', (value) => {
|
|
70
|
+
expect(isEmptyContainer(value)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe, expect, test,
|
|
3
|
+
} from 'vitest';
|
|
4
|
+
import { isEqualPath } from '../../src/assertions/index.js';
|
|
5
|
+
|
|
6
|
+
describe('isEqualPath', () => {
|
|
7
|
+
test.each([
|
|
8
|
+
[[0], []],
|
|
9
|
+
[[0], [1]],
|
|
10
|
+
[[0, 'props', 'items'], [0, 'props', 'components']],
|
|
11
|
+
])('must return false on non-equal pathes: %p === %p', (path1, path2) => {
|
|
12
|
+
expect(isEqualPath(path1, path2)).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test.each([
|
|
16
|
+
[[], []],
|
|
17
|
+
[[1], [1]],
|
|
18
|
+
[['components'], ['components']],
|
|
19
|
+
[[0, 'props', 'components'], [0, 'props', 'components']],
|
|
20
|
+
])('must return true on equal pathes: %p === %p', (path1, path2) => {
|
|
21
|
+
expect(isEqualPath(path1, path2)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe, expect, test,
|
|
3
|
+
} from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
isYArray,
|
|
6
|
+
isYMap,
|
|
7
|
+
} from '../../src/assertions/index.js';
|
|
8
|
+
|
|
9
|
+
import * as Y from 'yjs';
|
|
10
|
+
|
|
11
|
+
const object = {
|
|
12
|
+
a: 2,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('isYMap', () => {
|
|
16
|
+
test.each(['string', 4, object, null, new Y.Array()])('must return false on non YMap types: %p', (value) => {
|
|
17
|
+
expect(isYMap(value)).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('must return true on YMap types', () => {
|
|
21
|
+
expect(isYMap(new Y.Map())).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('isYArray', () => {
|
|
26
|
+
test.each(['[]', object, [], null, new Y.Map()])('must return false on non YArray types: %p', (value) => {
|
|
27
|
+
expect(isYArray(value)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('must return true on YArray types', () => {
|
|
31
|
+
expect(isYArray(new Y.Array())).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { getComponentsSubtreeByPath, getComponentsSubtreeByRPath } from '../../src/dynamic-components/index.js';
|
|
3
|
+
|
|
4
|
+
describe('getComponentsSubtreeByRPath', () => {
|
|
5
|
+
test('must extract subtree from collection', () => {
|
|
6
|
+
const components = [
|
|
7
|
+
{
|
|
8
|
+
id: 'xxx',
|
|
9
|
+
component: 'collection',
|
|
10
|
+
props: {
|
|
11
|
+
components: [
|
|
12
|
+
{
|
|
13
|
+
id: 'yyy',
|
|
14
|
+
component: 'text-input',
|
|
15
|
+
props: {
|
|
16
|
+
label: 'Title',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const { pathWithComponents, complete } = getComponentsSubtreeByRPath(components, ['xxx', 'yyy']);
|
|
25
|
+
expect(complete).toBeTruthy();
|
|
26
|
+
|
|
27
|
+
expect(pathWithComponents).toMatchObject([
|
|
28
|
+
{
|
|
29
|
+
id: 'xxx',
|
|
30
|
+
component: 'collection',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'yyy',
|
|
34
|
+
component: 'text-input',
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('must extract subtree from repeatable collection', () => {
|
|
40
|
+
const components = [
|
|
41
|
+
{
|
|
42
|
+
id: 'xxx',
|
|
43
|
+
component: 'repeatable-collection',
|
|
44
|
+
props: {
|
|
45
|
+
name: 'fields',
|
|
46
|
+
components: [
|
|
47
|
+
{
|
|
48
|
+
id: 'yyy',
|
|
49
|
+
component: 'text-input',
|
|
50
|
+
props: {
|
|
51
|
+
name: 'title',
|
|
52
|
+
label: 'Title',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const { pathWithComponents, complete } = getComponentsSubtreeByRPath(components, ['xxx', 'yyy']);
|
|
61
|
+
expect(complete).toBeTruthy();
|
|
62
|
+
|
|
63
|
+
expect(pathWithComponents).toMatchObject([
|
|
64
|
+
{
|
|
65
|
+
id: 'xxx',
|
|
66
|
+
component: 'repeatable-collection',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'yyy',
|
|
70
|
+
component: 'text-input',
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('deepGetComponentsSubtreeByPath', () => {
|
|
77
|
+
test('must extract subtree from collection', () => {
|
|
78
|
+
const components = [
|
|
79
|
+
{
|
|
80
|
+
id: 'xxx',
|
|
81
|
+
component: 'collection',
|
|
82
|
+
props: {
|
|
83
|
+
name: 'seo',
|
|
84
|
+
components: [
|
|
85
|
+
{
|
|
86
|
+
id: 'yyy',
|
|
87
|
+
component: 'text-input',
|
|
88
|
+
props: {
|
|
89
|
+
name: 'title',
|
|
90
|
+
label: 'Title',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const { pathWithComponents, complete } = getComponentsSubtreeByPath(components, ['seo', 'title']);
|
|
99
|
+
expect(complete).toBeTruthy();
|
|
100
|
+
|
|
101
|
+
expect(pathWithComponents).toMatchObject([
|
|
102
|
+
{
|
|
103
|
+
id: 'xxx',
|
|
104
|
+
component: 'collection',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'yyy',
|
|
108
|
+
component: 'text-input',
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('must extract subtree from repeatable collection', () => {
|
|
114
|
+
const components = [
|
|
115
|
+
{
|
|
116
|
+
id: 'xxx',
|
|
117
|
+
component: 'repeatable-collection',
|
|
118
|
+
props: {
|
|
119
|
+
name: 'fields',
|
|
120
|
+
components: [
|
|
121
|
+
{
|
|
122
|
+
id: 'yyy',
|
|
123
|
+
component: 'text-input',
|
|
124
|
+
props: {
|
|
125
|
+
name: 'title',
|
|
126
|
+
label: 'Title',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const { pathWithComponents, complete } = getComponentsSubtreeByPath(components, ['fields', 'id:xxx', 'title']);
|
|
135
|
+
expect(complete).toBeTruthy();
|
|
136
|
+
|
|
137
|
+
expect(pathWithComponents).toMatchObject([
|
|
138
|
+
{
|
|
139
|
+
id: 'xxx',
|
|
140
|
+
component: 'repeatable-collection',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: 'yyy',
|
|
144
|
+
component: 'text-input',
|
|
145
|
+
},
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('must extract subtree by id', () => {
|
|
150
|
+
const components = [
|
|
151
|
+
{
|
|
152
|
+
id: 'e97ea',
|
|
153
|
+
component: 'text-input',
|
|
154
|
+
props: {
|
|
155
|
+
name: 'title',
|
|
156
|
+
label: 'Title',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const { pathWithComponents, complete } = getComponentsSubtreeByPath(components, ['e97ea']);
|
|
162
|
+
expect(complete).toBeTruthy();
|
|
163
|
+
|
|
164
|
+
expect(pathWithComponents).toMatchObject([
|
|
165
|
+
{
|
|
166
|
+
id: 'e97ea',
|
|
167
|
+
component: 'text-input',
|
|
168
|
+
},
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { getDataObjectKey } from '@/data-objects/index.js';
|
|
2
|
+
import { JSONContent, getSchema } from '@tiptap/core';
|
|
3
|
+
import { default as StarterKit } from '@tiptap/starter-kit';
|
|
4
|
+
import { describe, expect, test } from 'vitest';
|
|
5
|
+
import { prosemirrorJSONToYXmlFragment } from 'y-prosemirror';
|
|
6
|
+
import * as Y from 'yjs';
|
|
7
|
+
import {
|
|
8
|
+
DRichText, DTextInput, DynamicComponent, DynamicComponentWithPosition, copyDynamicComponent, makeDynamicComponentsWithPosition,
|
|
9
|
+
} from '../../src/dynamic-components/index.js';
|
|
10
|
+
|
|
11
|
+
describe('copyDynamicComponent', () => {
|
|
12
|
+
test('must copy component', () => {
|
|
13
|
+
const component: DTextInput = {
|
|
14
|
+
id: 'xxx',
|
|
15
|
+
component: 'text-input',
|
|
16
|
+
props: {
|
|
17
|
+
name: 'email',
|
|
18
|
+
label: 'Email',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const componentCopy = copyDynamicComponent(component);
|
|
23
|
+
|
|
24
|
+
expect(componentCopy).toEqual({
|
|
25
|
+
...component,
|
|
26
|
+
id: expect.any(String),
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('must copy component with rich text', () => {
|
|
31
|
+
const jsonContent: JSONContent = {
|
|
32
|
+
type: 'doc',
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: 'paragraph',
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: 'Hello World!',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const component: DRichText = {
|
|
47
|
+
id: 'xxx',
|
|
48
|
+
component: 'rich-text',
|
|
49
|
+
props: {
|
|
50
|
+
content: {
|
|
51
|
+
id: 'yyy',
|
|
52
|
+
type: 'RichText',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const schema = getSchema([
|
|
58
|
+
StarterKit,
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const key = getDataObjectKey(component.props.content);
|
|
62
|
+
|
|
63
|
+
const yDoc = new Y.Doc();
|
|
64
|
+
prosemirrorJSONToYXmlFragment(
|
|
65
|
+
schema,
|
|
66
|
+
jsonContent,
|
|
67
|
+
yDoc.getXmlFragment(key),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const componentCopy = copyDynamicComponent(component, yDoc) as DRichText;
|
|
71
|
+
|
|
72
|
+
const newKey = getDataObjectKey(componentCopy.props.content);
|
|
73
|
+
|
|
74
|
+
expect(yDoc.getXmlFragment(newKey).toJSON())
|
|
75
|
+
.toEqual(yDoc.getXmlFragment(key).toJSON());
|
|
76
|
+
|
|
77
|
+
expect(componentCopy).toEqual({
|
|
78
|
+
...component,
|
|
79
|
+
props: {
|
|
80
|
+
...component.props,
|
|
81
|
+
content: {
|
|
82
|
+
...component.props.content,
|
|
83
|
+
id: expect.any(String),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
id: expect.any(String),
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('makeDynamicComponentsWithPosition', () => {
|
|
92
|
+
test('should return dynamic components with position', () => {
|
|
93
|
+
const components: DynamicComponent[] = [1, 2, 3].map((n) => ({
|
|
94
|
+
id: `${n}`,
|
|
95
|
+
component: `component${n}`,
|
|
96
|
+
props: {
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
const expected: DynamicComponentWithPosition[] = [1, 2, 3].map((n) => ({
|
|
101
|
+
id: `${n}`,
|
|
102
|
+
component: `component${n}`,
|
|
103
|
+
position: `a${n - 1}`, // a0, a1, a2
|
|
104
|
+
props: {
|
|
105
|
+
},
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
const result = makeDynamicComponentsWithPosition(components);
|
|
109
|
+
|
|
110
|
+
expect(result).toEqual(expected);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('should return empty array if no components provided', () => {
|
|
114
|
+
const components: DynamicComponent[] = [];
|
|
115
|
+
const expected: DynamicComponentWithPosition[] = [];
|
|
116
|
+
|
|
117
|
+
const result = makeDynamicComponentsWithPosition(components);
|
|
118
|
+
|
|
119
|
+
expect(result).toEqual(expected);
|
|
120
|
+
});
|
|
121
|
+
});
|