@cocreate/elements 1.46.0 → 1.48.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.
@@ -8,8 +8,11 @@ import CRUD from "@cocreate/crud-client";
8
8
 
9
9
  import { elements, keys, debounce } from "./mapRegistry.js";
10
10
  import { getDataObject } from "./getDataHandler.js";
11
- import { setData } from "./setDataHandler.js";
12
- import { deriveSubscriptionKeys } from "./subscribeHandler.js";
11
+ import { setData, setDataType } from "./setDataHandler.js";
12
+ import {
13
+ deriveSubscriptionKeys,
14
+ getSubscription
15
+ } from "./subscribeHandler.js";
13
16
 
14
17
  /**
15
18
  * Reads data for one or more elements based on their dataKey scope.
@@ -26,8 +29,12 @@ export async function read(element, data, dataKey) {
26
29
  data = { ...existingData.dataKey.object };
27
30
  }
28
31
 
29
- if (!dataKey || !data.type) return;
30
- if (!data.$filter && (!data[data.type] || !data[data.type].length)) return;
32
+ if (!dataKey || !data?.type) {
33
+ return;
34
+ }
35
+ if (!data.filter && !data[data.type]) {
36
+ return;
37
+ }
31
38
 
32
39
  if (!(element instanceof HTMLCollection) && !Array.isArray(element))
33
40
  element = [element];
@@ -42,12 +49,10 @@ export async function read(element, data, dataKey) {
42
49
  if (
43
50
  isRead !== "false" &&
44
51
  !el.hasRead &&
45
- (!data.$filter || (data.$filter && data.$filter.isFilter !== false))
52
+ (!data.filter || (data.filter && data.filter.isFilter !== false))
46
53
  ) {
47
54
  el.hasRead = true;
48
55
  delay.elements.set(el, true);
49
- } else {
50
- // console.log("read skipped", el);
51
56
  }
52
57
  }
53
58
 
@@ -55,26 +60,39 @@ export async function read(element, data, dataKey) {
55
60
 
56
61
  clearTimeout(delay.timer);
57
62
  delay.timer = setTimeout(function () {
58
- if (!data.$filter && data.type === "object") {
59
- if (!data.object) return;
63
+ // Detach this batch before sending so reads queued while the request is
64
+ // in flight cannot mutate (or later delete) the same element map.
65
+ const els = Array.from(delay.elements?.keys() || []);
66
+ if (debounce.get(dataKey.string) === delay)
67
+ debounce.delete(dataKey.string);
68
+ clearTimeout(delay.timer);
69
+ delete delay.elements;
70
+ delete delay.timer;
71
+
72
+ if (!els.length) return;
73
+
74
+ if (!data.filter && data.type === "object") {
75
+ if (!data.object) {
76
+ return;
77
+ }
60
78
  else if (typeof data.object === "string")
61
79
  data.object = { _id: data.object };
62
80
  else if (Array.isArray(data.object)) {
63
81
  if (data.object.length)
64
82
  data.object = { _id: data.object[0]._id };
65
- } else if (!data.object._id.match(/^[0-9a-fA-F]{24}$/)) return;
83
+ } else if (!data.object._id.match(/^[0-9a-fA-F]{24}$/)) {
84
+ return;
85
+ }
66
86
  }
67
87
 
68
88
  data.method = data.type + ".read";
89
+ const subscription = getSubscription(data);
90
+ if (subscription) data.subscription = subscription.string;
69
91
 
70
- CRUD.send(data).then((responseData) => {
71
- debounce.delete(dataKey.string);
72
- let els = Array.from(delay.elements.keys());
73
- clearTimeout(delay.timer);
74
- delete delay.elements;
75
- delete delay.timer;
76
-
77
- setData(els, responseData);
92
+ CRUD.send(data).then(async (responseData) => {
93
+ if (responseData) {
94
+ await setData(els, responseData);
95
+ }
78
96
  });
79
97
  }, 500);
80
98
  }
@@ -111,8 +129,17 @@ export async function save(element, action) {
111
129
 
112
130
  value = await element.getValue();
113
131
  let key = element.getAttribute("key");
114
-
115
- if (typeof data[data.type] === "string")
132
+ if (data.type === "object" && !data.object) {
133
+ if (key == "{}") {
134
+ if (Array.isArray(value)) {
135
+ data[data.type] = value.map((v) => ({ ...v }));
136
+ } else
137
+ data[data.type] = value;
138
+ } else {
139
+ data[data.type] = { [key]: value };
140
+ }
141
+ }
142
+ else if (typeof data[data.type] === "string")
116
143
  if (key == "{}")
117
144
  data[data.type] = { _id: data[data.type], ...value };
118
145
  else data[data.type] = { _id: data[data.type], [key]: value };
@@ -183,19 +210,26 @@ export async function save(element, action) {
183
210
  }
184
211
 
185
212
  // Derive and attach subscriptionKeys array (contains specific document key and list key)
213
+ const subscription = getSubscription(data[i]);
214
+ if (subscription) data[i].subscription = subscription.string;
186
215
  data[i].subscriptionKeys = deriveSubscriptionKeys(data[i]);
187
216
 
188
217
  data[i] = await CRUD.send(data[i]);
218
+ const errors = data[i]?.error;
219
+ if (!data[i] || (Array.isArray(errors) ? errors.length > 0 : !!errors)) {
220
+ const message = Array.isArray(errors)
221
+ ? errors.map((error) => error.message || String(error)).join("; ")
222
+ : errors?.message || String(errors || "Save failed");
223
+ throw new Error(message);
224
+ }
189
225
 
190
226
  responseDataList.push(data[i]);
191
227
 
192
228
  if (
193
229
  data[i] &&
194
- (data[i].method.endsWith(".create") ||
195
- (data[i].type !== "object" &&
196
- data[i].method.endsWith(".update")))
230
+ (data[i].method.endsWith(".create") || data[i].method.endsWith(".update"))
197
231
  ) {
198
- setDataType(element, data[i], action);
232
+ await setDataType(element, data[i], action);
199
233
  } else if (data[i]) {
200
234
  if (action) {
201
235
  action.element.dispatchEvent(
@@ -220,8 +254,49 @@ Actions.init([
220
254
  {
221
255
  name: "save",
222
256
  endEvent: "saved",
223
- callback: (action) => {
224
- if (action.form || action) save(action.form, action);
257
+ callback: async (action) => {
258
+ if (action.element.hasAttribute("save-query")) {
259
+ const targets = queryElements({
260
+ element: action.element,
261
+ prefix: "save"
262
+ });
263
+
264
+ if (!targets.length) {
265
+ action.abort({
266
+ code: "save-target-not-found",
267
+ message: "The save target could not be found.",
268
+ selector: action.element.attributes.getNamedItem("save-query")?.value
269
+ });
270
+ return;
271
+ }
272
+
273
+ try {
274
+ const result = [];
275
+ for (const target of targets) {
276
+ result.push(await save(target));
277
+ }
278
+ action.end({ result });
279
+ } catch (error) {
280
+ action.abort({
281
+ code: "save-failed",
282
+ message: "Unable to save the form.",
283
+ cause: error
284
+ });
285
+ console.error("Save action failed:", error);
286
+ }
287
+ return;
288
+ }
289
+
290
+ try {
291
+ await save(action.form || action.element, action);
292
+ } catch (error) {
293
+ action.abort({
294
+ code: "save-failed",
295
+ message: "Unable to save the form.",
296
+ cause: error
297
+ });
298
+ console.error("Save action failed:", error);
299
+ }
225
300
  }
226
301
  },
227
302
  {
@@ -238,34 +313,63 @@ Actions.init([
238
313
  elements = [action.element];
239
314
  }
240
315
 
316
+ const groups = new Map();
241
317
  for (let i = 0; i < elements.length; i++) {
242
318
  const data = getDataObject(elements[i]);
243
- if (!data) return;
319
+ if (!data) continue;
244
320
  data.method = data.type + ".delete";
245
321
 
246
- if (elements[i].renderValue) {
247
- let selected = elements[i].querySelectorAll(".selected");
248
- data[data.type] = [];
249
- for (let j = 0; j < selected.length; j++) {
250
- let attribute = selected[j].getAttribute(data.type);
251
- if (attribute) {
252
- attribute = attribute.split(",");
253
- for (let k = 0; k < attribute.length; k++) {
254
- if (data.type === "object")
255
- data[data.type].push({ _id: attribute[k] });
256
- else {
257
- data[data.type].push(attribute[k]);
258
- }
259
- }
260
- }
261
- }
262
- } else if (
322
+ if (
263
323
  data.type === "object" &&
264
324
  typeof data[data.type] === "string"
265
325
  )
266
326
  data[data.type] = { _id: data[data.type] };
267
327
 
268
- // Attach subscriptionKeys before sending delete payload
328
+ if (data.type !== "object") {
329
+ groups.set(Symbol(), data);
330
+ continue;
331
+ }
332
+
333
+ const groupKey = JSON.stringify({
334
+ storage: data.storage || "",
335
+ database: data.database || "",
336
+ array: data.array
337
+ });
338
+ const objects = Array.isArray(data.object)
339
+ ? data.object
340
+ : [data.object];
341
+ let group = groups.get(groupKey);
342
+
343
+ if (!group) {
344
+ group = {
345
+ ...data,
346
+ object: [],
347
+ objectIds: new Set()
348
+ };
349
+ groups.set(groupKey, group);
350
+ }
351
+
352
+ for (const object of objects) {
353
+ if (!object || !object._id || group.objectIds.has(object._id))
354
+ continue;
355
+ group.objectIds.add(object._id);
356
+ group.object.push({ _id: object._id });
357
+ }
358
+ }
359
+
360
+ for (const data of groups.values()) {
361
+ if (data.objectIds) {
362
+ delete data.objectIds;
363
+ if (!data.object.length) continue;
364
+ }
365
+
366
+ // Subscribe the requester at collection scope. A grouped object
367
+ // array is a delete payload, not a subscribable document key.
368
+ const subscriptionData = { ...data };
369
+ delete subscriptionData.object;
370
+ const subscription = getSubscription(subscriptionData);
371
+ if (subscription)
372
+ data.subscription = subscription.string;
269
373
  data.subscriptionKeys = deriveSubscriptionKeys(data);
270
374
 
271
375
  let response = await CRUD.send(data);
@@ -279,4 +383,4 @@ Actions.init([
279
383
  }
280
384
  ]);
281
385
 
282
- export default { read, save };
386
+ export default { read, save };
package/src/dndHandler.js CHANGED
@@ -2,7 +2,8 @@ import { dotNotationToObject } from "@cocreate/utils";
2
2
  import CRUD from "@cocreate/crud-client";
3
3
  import Observer from "@cocreate/observer";
4
4
  import { render, renderValue, renderedNodes } from "@cocreate/render";
5
- import { elements, keys, debounce } from "./mapRegistry.js"
5
+ import { elements, keys, debounce } from "./mapRegistry.js";
6
+ import { getDataElements, getDataObject } from "./getDataHandler.js";
6
7
 
7
8
  // TODO: has the potential to work on most of the crud elements specifically if the value is an object or an array
8
9
  async function dndCrud(draggedEl, draggedFrom, droppedEl, droppedIn) {
@@ -69,12 +70,12 @@ async function dndCrudData(element, parent, operator) {
69
70
 
70
71
  function dndNewData(element, data) {
71
72
  let dragData = {};
72
- if (data.$filter.query) {
73
- dndNewDataUpdate(dragData, data.$filter.query);
73
+ if (data.filter.query) {
74
+ dndNewDataUpdate(dragData, data.filter.query);
74
75
  }
75
76
 
76
77
  let sortName, sortDirection;
77
- let sort = data.$filter.sort;
78
+ let sort = data.filter.sort;
78
79
  if (sort && sort.length) {
79
80
  for (let i = sort.length - 1; i >= 0; i--) {
80
81
  if (typeof data[sort.key] === "number") {
@@ -89,7 +90,9 @@ function dndNewData(element, data) {
89
90
 
90
91
  let renderedNode = renderedNodes.get(element);
91
92
  if (renderedNode) {
92
- keyPath = renderedNode.keyPath;
93
+ const renderKeys = renderedNode.keys || {};
94
+ const activeKey = Object.keys(renderKeys).at(-1);
95
+ keyPath = activeKey ? renderKeys[activeKey].path : undefined;
93
96
  clonesMap = renderedNode.template.clones;
94
97
  clones = Array.from(clonesMap.values());
95
98
  index = clones.indexOf(element);
@@ -141,9 +144,9 @@ Observer.init({
141
144
  let renderedNode = renderedNodes.get(mutation.target);
142
145
  if (!renderedNode) return;
143
146
 
144
- if (!mutation.movedFrom) return;
145
- let draggedEl = mutation.target;
146
- let draggedFrom = mutation.movedFrom.parentElement;
147
+ if (!mutation.move || mutation.move.isDescendant) return;
148
+ let draggedEl = mutation.move.movedElement;
149
+ let draggedFrom = mutation.move.from.parentElement;
147
150
  let droppedEl =
148
151
  mutation.target.nextElementSibling ||
149
152
  mutation.target.previousElementSibling;