@cocreate/elements 1.46.0 → 2.0.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.
@@ -0,0 +1,31 @@
1
+ const { test } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const vm = require('node:vm');
5
+
6
+ const source = fs.readFileSync(require.resolve('../src/crudHandler.js'), 'utf8');
7
+ const saveSource = source.slice(source.indexOf('export async function save('), source.indexOf('\nActions.init(')).replace('export ', '');
8
+
9
+ for (const value of [[{ name: 'image.svg' }], [{ name: 'a.svg' }, { name: 'b.svg' }], { name: 'image.svg' }]) {
10
+ test(`saving an entire document preserves its shape: ${JSON.stringify(value)}`, async () => {
11
+ let sent;
12
+ const element = {
13
+ tagName: 'INPUT',
14
+ getAttribute: name => name === 'key' ? '{}' : null,
15
+ getValue: async () => value,
16
+ setAttribute() {}
17
+ };
18
+ const save = vm.runInNewContext(`(${saveSource})`, {
19
+ elements: new Map([[element, 'files']]),
20
+ keys: new Map([['files', { dataKey: { object: { type: 'object', array: 'files', object: '', key: '{}' } } }]]),
21
+ getSubscription: () => null,
22
+ deriveSubscriptionKeys: () => [],
23
+ CRUD: { send: async data => { sent = data; } }
24
+ });
25
+ await save(element);
26
+ assert.equal(Array.isArray(sent.object), Array.isArray(value));
27
+ assert.equal(JSON.stringify(sent.object), JSON.stringify(value));
28
+ assert.equal(sent.method, 'object.create');
29
+ if (Array.isArray(value)) assert.notEqual(sent.object, value);
30
+ });
31
+ }
@@ -0,0 +1,36 @@
1
+ const assert = require('node:assert/strict');
2
+ const { readFileSync } = require('node:fs');
3
+ const { join } = require('node:path');
4
+ const vm = require('node:vm');
5
+ const test = require('node:test');
6
+
7
+ test('save waits for the response to be applied before its caller continues', async () => {
8
+ const source = readFileSync(join(__dirname, '../src/crudHandler.js'), 'utf8');
9
+ const saveCode = source.slice(source.indexOf('export async function save('), source.indexOf('\nActions.init(')).replace('export ', '');
10
+ let finishApplying;
11
+ let applied = false;
12
+ let completed = false;
13
+ const gate = new Promise(resolve => { finishApplying = resolve; });
14
+ const form = {
15
+ tagName: 'FORM',
16
+ getAttribute: () => null,
17
+ setAttribute() {},
18
+ getData: async () => [{ type: 'object', array: 'clusters', object: { name: 'Cluster' } }]
19
+ };
20
+ const save = vm.runInNewContext(`(${saveCode})`, {
21
+ getSubscription: () => null,
22
+ deriveSubscriptionKeys: () => [],
23
+ CRUD: { send: async () => ({
24
+ type: 'object', method: 'object.create', array: 'clusters',
25
+ object: [{ _id: '65f8cb447b39a3c1a8904e22' }]
26
+ }) },
27
+ setDataType: async () => { await gate; applied = true; }
28
+ });
29
+ const completion = save(form).then(() => { completed = true; });
30
+ await new Promise(resolve => setImmediate(resolve));
31
+ assert.equal(completed, false);
32
+ finishApplying();
33
+ await completion;
34
+ assert.equal(applied, true);
35
+ assert.equal(completed, true);
36
+ });
@@ -0,0 +1,119 @@
1
+ const assert = require("node:assert/strict");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+ const vm = require("node:vm");
5
+ const test = require("node:test");
6
+
7
+ async function loadSetDataType(watchedElements = []) {
8
+ const context = vm.createContext({
9
+ console,
10
+ HTMLCollection: class {},
11
+ CustomEvent: class {
12
+ constructor(name, options) {
13
+ this.name = name;
14
+ this.detail = options?.detail;
15
+ }
16
+ }
17
+ });
18
+ const source = fs.readFileSync(
19
+ path.join(__dirname, "../src/setDataHandler.js"),
20
+ "utf8"
21
+ );
22
+ const module = new vm.SourceTextModule(source, { context });
23
+ const modules = {
24
+ "@cocreate/utils": {
25
+ dotNotationToObject: value => value,
26
+ queryData: () => true,
27
+ searchData: () => true,
28
+ sortData: value => value,
29
+ getAttributes: element => ({ key: element.getAttribute('key') }),
30
+ checkValue: () => true,
31
+ getValueFromObject: () => undefined
32
+ },
33
+ "@cocreate/crud-client": { default: { getValueFromObject: (object, key) => object[key] } },
34
+ "@cocreate/filter": { default: { filters: new Map() } },
35
+ "./mapRegistry.js": {
36
+ elements: new WeakMap(),
37
+ forms: new WeakMap(),
38
+ keys: new Map()
39
+ },
40
+ "./getDataHandler.js": {
41
+ getDataElements: () => watchedElements,
42
+ getDataObject: () => undefined
43
+ }
44
+ };
45
+
46
+ await module.link(name => new vm.SyntheticModule(
47
+ Object.keys(modules[name]),
48
+ function () {
49
+ for (const [key, value] of Object.entries(modules[name])) {
50
+ this.setExport(key, value);
51
+ }
52
+ },
53
+ { context }
54
+ ));
55
+ await module.evaluate();
56
+ return module.namespace.setDataType;
57
+ }
58
+
59
+ test("setDataType tolerates a form that has not entered the registry yet", async () => {
60
+ const setDataType = await loadSetDataType();
61
+ const attributes = { array: "apis", object: "pending" };
62
+ const form = {
63
+ tagName: "FORM",
64
+ querySelectorAll: () => [],
65
+ getAttribute: name => attributes[name] ?? null,
66
+ hasAttribute: name => Object.hasOwn(attributes, name),
67
+ setAttribute: (name, value) => {
68
+ attributes[name] = value;
69
+ }
70
+ };
71
+
72
+ await assert.doesNotReject(() => setDataType(form, {
73
+ type: "object",
74
+ object: [{
75
+ _id: "6abb2ce702c3c7e6fbec0215",
76
+ $array: "apis"
77
+ }]
78
+ }, { element: { dispatchEvent() {} } }));
79
+ assert.equal(attributes.object, "6abb2ce702c3c7e6fbec0215");
80
+ });
81
+
82
+ test('saved is dispatched only after response fields following async work are updated', async () => {
83
+ let release;
84
+ let idValue = '';
85
+ const gate = new Promise(resolve => { release = resolve; });
86
+ const watcher = attributes => ({
87
+ hasAttribute: name => Object.hasOwn(attributes, name),
88
+ getAttribute: name => attributes[name] ?? null,
89
+ querySelector: () => null,
90
+ dispatchEvent() {}
91
+ });
92
+ const renderer = watcher({ 'render-query': 'template' });
93
+ renderer.renderValue = async () => {};
94
+ renderer.getData = () => gate;
95
+ const idField = watcher({ key: '_id' });
96
+ idField.setValue = value => { idValue = value; };
97
+ const setDataType = await loadSetDataType([renderer, idField]);
98
+ const attributes = { array: 'clusters', object: 'pending' };
99
+ const form = {
100
+ tagName: 'FORM',
101
+ querySelectorAll: () => [],
102
+ hasAttribute: name => Object.hasOwn(attributes, name),
103
+ getAttribute: name => attributes[name] ?? null,
104
+ setAttribute: (name, value) => { attributes[name] = value; }
105
+ };
106
+ const id = '65f8cb447b39a3c1a8904e22';
107
+ const savedIds = [];
108
+ const completion = setDataType(form, {
109
+ type: 'object', method: 'object.create', array: 'clusters',
110
+ object: [{ _id: id, $array: 'clusters' }]
111
+ }, { element: { dispatchEvent: () => savedIds.push(idValue) } });
112
+ await new Promise(resolve => setImmediate(resolve));
113
+ assert.equal(idValue, '');
114
+ assert.deepEqual(savedIds, []);
115
+ release();
116
+ await completion;
117
+ assert.equal(idValue, id);
118
+ assert.deepEqual(savedIds, [id]);
119
+ });
@@ -0,0 +1,108 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const vm = require('node:vm');
5
+ const test = require('node:test');
6
+
7
+ async function loadSetData() {
8
+ const context = vm.createContext({ console, HTMLCollection: class {}, CustomEvent: class {} });
9
+ const source = fs.readFileSync(path.join(__dirname, '../src/setDataHandler.js'), 'utf8');
10
+ const module = new vm.SourceTextModule(source, { context });
11
+ const utils = {
12
+ dotNotationToObject: value => value, queryData: () => true,
13
+ searchData: () => true, sortData: value => value,
14
+ getAttributes: element => ({ key: element.getAttribute('key'), isRead: element.getAttribute('read') }),
15
+ checkValue: () => true,
16
+ getValueFromObject: (object, key) => object[key]
17
+ };
18
+ const modules = {
19
+ '@cocreate/utils': utils,
20
+ '@cocreate/crud-client': { default: { getValueFromObject: (object, key) => object[key] } },
21
+ '@cocreate/filter': { default: { filters: new Map() } },
22
+ './mapRegistry.js': { elements: new WeakMap(), forms: new WeakMap(), keys: new Map() },
23
+ './getDataHandler.js': { getDataElements: () => [], getDataObject: element => ({ object: element.getAttribute('object') }) }
24
+ };
25
+ await module.link(name => new vm.SyntheticModule(Object.keys(modules[name]), function () {
26
+ for (const [key, value] of Object.entries(modules[name])) this.setExport(key, value);
27
+ }, { context }));
28
+ await module.evaluate();
29
+ return module.namespace.setData;
30
+ }
31
+
32
+ function element(attributes) {
33
+ return {
34
+ hasAttribute: name => Object.hasOwn(attributes, name),
35
+ getAttribute: name => attributes[name] ?? null,
36
+ removeAttribute: name => delete attributes[name],
37
+ querySelector: () => null,
38
+ dispatchEvent() {},
39
+ values: [],
40
+ setValue(value) { this.values.push(value); }
41
+ };
42
+ }
43
+
44
+ test('SSR app labels do not prevent later table columns from receiving shared data', async () => {
45
+ const setData = await loadSetData();
46
+ const id = '28b6ddbe8b35de252ecd79e1';
47
+ const label = element({ array: 'apps', object: id, key: 'name', rendered: 'true' });
48
+ const table = element({ array: 'apps', object: id, key: 'fields', 'render-query': "[template='column'][]" });
49
+ const renders = [];
50
+ table.renderValue = async data => renders.push(data);
51
+ const fields = [{ key: 'name', label: 'Name' }, { key: 'type', label: 'Type' }];
52
+ await setData([label, table], {
53
+ method: 'object.read', array: 'apps',
54
+ object: [{ _id: id, $storage: ['mongodb'], name: 'Cluster', fields }]
55
+ });
56
+ assert.equal(label.hasAttribute('rendered'), false);
57
+ assert.deepEqual(label.values, []);
58
+ assert.equal(renders.length, 1);
59
+ assert.deepEqual(Array.from(renders[0].fields), fields);
60
+ });
61
+
62
+ test('filtered counts retain their value during writes and refresh each cluster independently', async () => {
63
+ const setData = await loadSetData();
64
+ const counts = ['cluster-a', 'cluster-b'].map(cluster => {
65
+ const el = element({ array: 'servers', object: '', key: '$length' });
66
+ el.getFilter = async () => ({ query: { cluster_id: cluster }, index: 0 });
67
+ el.refreshes = [];
68
+ el.setFilter = filter => el.refreshes.push(filter);
69
+ return el;
70
+ });
71
+ for (const el of counts) {
72
+ await setData(el, { method: 'object.read', array: 'servers', object: [{ _id: 'server' }] });
73
+ }
74
+ for (const method of ['object.create', 'object.update', 'object.delete']) {
75
+ await setData(counts, { method, array: 'servers', object: [{ _id: 'server', $update: { cluster_id: 'cluster-b' } }] });
76
+ }
77
+ for (let i = 0; i < counts.length; i++) {
78
+ assert.deepEqual(counts[i].values, [1]);
79
+ assert.equal(counts[i].refreshes.length, 3);
80
+ assert.equal(counts[i].refreshes[0].query.cluster_id, ['cluster-a', 'cluster-b'][i]);
81
+ }
82
+ await setData(counts[0], { method: 'object.read', array: 'servers', object: [], error: [{ message: 'failed' }] });
83
+ assert.deepEqual(counts[0].values, [1]);
84
+ await setData(counts[0], { method: 'object.read', array: 'servers', object: [] });
85
+ assert.deepEqual(counts[0].values, [1, 0]);
86
+ });
87
+
88
+ test('unmatched document updates do not write the empty object attribute into a value', async () => {
89
+ const setData = await loadSetData();
90
+ const el = element({ array: 'servers', object: '', key: '$length' });
91
+ el.getFilter = async () => ({});
92
+ await setData(el, { method: 'object.update', array: 'servers', object: [{ _id: 'other' }] });
93
+ assert.deepEqual(el.values, []);
94
+ });
95
+
96
+ test('read=false keeps a deferred value expression intact when a save response arrives', async () => {
97
+ const setData = await loadSetData();
98
+ const expression = '$query($closest #provision-workflow #cluster-object-id).getValue()';
99
+ const el = element({ key: 'cluster_id', read: 'false', value: expression });
100
+ el.setValue = value => { el.values.push(value); el.value = value; };
101
+ el.value = expression;
102
+ await setData(el, {
103
+ method: 'object.create', array: 'servers',
104
+ object: [{ _id: 'server', cluster_id: '65f8cb447b39a3c1a8904e22' }]
105
+ });
106
+ assert.equal(el.value, expression);
107
+ assert.deepEqual(el.values, []);
108
+ });
package/webpack.config.js CHANGED
@@ -48,6 +48,7 @@ module.exports = async (env, argv) => {
48
48
  minimizer: [
49
49
  new EsbuildPlugin({
50
50
  target: "es2017",
51
+ minify: isProduction,
51
52
  css: true
52
53
  })
53
54
  ],
package/src/dndHandler.js DELETED
@@ -1,155 +0,0 @@
1
- import { dotNotationToObject } from "@cocreate/utils";
2
- import CRUD from "@cocreate/crud-client";
3
- import Observer from "@cocreate/observer";
4
- import { render, renderValue, renderedNodes } from "@cocreate/render";
5
- import { elements, keys, debounce } from "./mapRegistry.js"
6
-
7
- // TODO: has the potential to work on most of the crud elements specifically if the value is an object or an array
8
- async function dndCrud(draggedEl, draggedFrom, droppedEl, droppedIn) {
9
- let from = await dndCrudData(draggedEl, draggedFrom, "remove");
10
- let to = await dndCrudData(droppedEl, droppedIn, "add");
11
-
12
- if (from && to && !draggedFrom.isSameNode(droppedIn)) {
13
- let element = getDataElements(from.data);
14
- if (!element.length) return;
15
-
16
- if (!element.includes(droppedIn)) {
17
- if (from.keyPath.includes(".")) {
18
- let test = { [from.keyPath]: undefined };
19
-
20
- from.clonedParentData = dotNotationToObject(test, from.clonedParentData);
21
- dndCrudSend(from.clonedParentData, "update");
22
- } else {
23
- const index = from.keyPath.match(/\[(\d+)\]/);
24
- let removeData = from.clonedParentData[from.clonedParentData.type].splice(
25
- index,
26
- 1
27
- )[0];
28
- if (removeData) {
29
- removeData.type = from.clonedParentData.type;
30
- removeData[removeData.type] = removeData;
31
- dndCrudSend(removeData, "delete");
32
- }
33
-
34
- if (from.clonedParentData[from.clonedParentData.type].length)
35
- dndCrudSend(from.clonedParentData, "update");
36
- }
37
- }
38
- }
39
-
40
- if (to) dndCrudSend(to.clonedParentData, "update");
41
- }
42
-
43
- async function dndCrudData(element, parent, operator) {
44
- if (!elements.has(parent)) return;
45
- let data = await parent.getData();
46
- let clonedParentData = getDataObject(parent);
47
-
48
- let { dragData, sortName, sortDirection, keyPath, clones, index } = dndNewData(
49
- element,
50
- data
51
- );
52
- clonedParentData[clonedParentData.type] = [];
53
-
54
- if (sortName) {
55
- for (let i = 0; i < clones.length; i++) {
56
- if (i > index) {
57
- let previousData = data[data.type][index];
58
- dragData[sortName] = operator === "add" ? i + 1 : i - 1;
59
-
60
- clonedParentData[clonedParentData.type].push({ ...previousData, ...dragData });
61
- }
62
- }
63
- } else {
64
- clonedParentData[clonedParentData.type] = [{ ...data[data.type][index], ...dragData }];
65
- }
66
-
67
- return { data, clonedParentData, keyPath, clones, index };
68
- }
69
-
70
- function dndNewData(element, data) {
71
- let dragData = {};
72
- if (data.$filter.query) {
73
- dndNewDataUpdate(dragData, data.$filter.query);
74
- }
75
-
76
- let sortName, sortDirection;
77
- let sort = data.$filter.sort;
78
- if (sort && sort.length) {
79
- for (let i = sort.length - 1; i >= 0; i--) {
80
- if (typeof data[sort.key] === "number") {
81
- sortName = sort.key;
82
- sortDirection = sort.direction;
83
- break;
84
- }
85
- }
86
- }
87
-
88
- let keyPath, clonesMap, clones, index;
89
-
90
- let renderedNode = renderedNodes.get(element);
91
- if (renderedNode) {
92
- keyPath = renderedNode.keyPath;
93
- clonesMap = renderedNode.template.clones;
94
- clones = Array.from(clonesMap.values());
95
- index = clones.indexOf(element);
96
- }
97
-
98
- return { dragData, sortName, sortDirection, keyPath, clones, index };
99
- }
100
-
101
- function dndNewDataUpdate(dragData, query) {
102
- for (let key of Object.keys(query)) {
103
- if (key === "$and" || key === "$or" || key === "$nor") {
104
- for (let i = 0; i < query[key].length; i++) {
105
- dndNewDataUpdate(dragData, query[key][i]);
106
- }
107
- } else if (
108
- typeof query[key] === "object" &&
109
- !Array.isArray(query[key])
110
- ) {
111
- if (query[key].$eq) dragData[key] = query[key].$eq;
112
- if (query[key].$ne) dragData[key] = query[key].$ne;
113
- } else {
114
- dragData[key] = query[key];
115
- }
116
- }
117
- }
118
-
119
- function dndCrudSend(data, crudType) {
120
- if (CRUD) {
121
- let action = data.type;
122
- action = action.charAt(0).toUpperCase() + action.slice(1);
123
- CRUD[crudType + action](data);
124
- } else console.log("dnd reordered data set as crud is unavailable");
125
- }
126
-
127
- Observer.init({
128
- name: "render",
129
- types: ["addedNodes"],
130
- selector: "[render-clone]",
131
- callback: function (mutation) {
132
- if (
133
- mutation.parentElement &&
134
- !mutation.parentElement.hasAttribute("dnd")
135
- )
136
- return;
137
- let delayTimer = debounce.get(mutation);
138
- clearTimeout(delayTimer);
139
- debounce.delete(mutation.target);
140
-
141
- let renderedNode = renderedNodes.get(mutation.target);
142
- if (!renderedNode) return;
143
-
144
- if (!mutation.movedFrom) return;
145
- let draggedEl = mutation.target;
146
- let draggedFrom = mutation.movedFrom.parentElement;
147
- let droppedEl =
148
- mutation.target.nextElementSibling ||
149
- mutation.target.previousElementSibling;
150
- let droppedIn = mutation.parentElement;
151
-
152
- if (!draggedFrom || !droppedIn) return;
153
- dndCrud(draggedEl, draggedFrom, droppedEl, droppedIn);
154
- }
155
- });