@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.
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  dotNotationToObject,
3
3
  queryData,
4
+ searchData,
4
5
  sortData,
5
6
  getAttributes,
6
7
  checkValue,
@@ -8,21 +9,25 @@ import {
8
9
  } from "@cocreate/utils";
9
10
  import CRUD from "@cocreate/crud-client";
10
11
  import filter from "@cocreate/filter";
11
- import { elements, forms, keys } from "./mapRegistry.js"
12
+ import { elements, forms, keys } from "./mapRegistry.js";
13
+ import { getDataElements, getDataObject } from "./getDataHandler.js";
12
14
 
13
- export function setDataType(element, data, action) {
15
+ export async function setDataType(element, data, action) {
14
16
  // TODO: if an array name is updated, the attibute array="" needs to be updated.
15
17
 
16
18
  if (!data) return;
17
19
 
20
+ const dataElements = new Set(getDataElements(data));
21
+
18
22
  let form;
19
23
  if (element.tagName === "FORM") {
20
24
  form = element;
21
- } else if (element.parentElement){
25
+ } else if (element.parentElement) {
22
26
  form = element.parentElement.closest("form");
23
27
  }
24
28
 
25
29
  if (!form) {
30
+ dataElements.add(element);
26
31
  if (data.type === "object") {
27
32
  if (data.object?.[0]?._id) {
28
33
  element.setAttribute("object", data.object[0]._id);
@@ -32,12 +37,31 @@ export function setDataType(element, data, action) {
32
37
  }
33
38
  } else {
34
39
  let formObject = forms.get(form);
35
- if (form.getAttribute("object") === "pending")
36
- form.setAttribute("object", data.object[0]._id);
40
+ const fieldsets = form.querySelectorAll('fieldset[object="pending"]');
41
+ for (const scope of [...fieldsets, form]) {
42
+ if (scope.getAttribute("object") === "pending") {
43
+ const object = data.object?.[0];
44
+ if (scope.hasAttribute("array")) {
45
+ const arrays = scope.getAttribute("array").split(",");
46
+ const objectArrays = Array.isArray(object?.$array)
47
+ ? object.$array
48
+ : [object?.$array];
49
+ if (object?._id && arrays.some((array) => {
50
+ const name = array.trim();
51
+ return name !== "" && objectArrays.includes(name);
52
+ })) {
53
+ scope.setAttribute("object", object._id);
54
+ }
55
+ } else if (object?._id) {
56
+ scope.setAttribute("object", object._id);
57
+ }
58
+ }
59
+ }
37
60
 
38
- let elements = formObject.types.get(data.type);
61
+ const typeElements = formObject?.types?.get(data.type);
39
62
 
40
- for (let [el, elData] of elements.entries()) {
63
+ for (let [el, elData] of typeElements?.entries() || []) {
64
+ dataElements.add(el);
41
65
  if (data.type === "object") {
42
66
  if (!elData.object || elData.object === "pending") {
43
67
  elData.object = data.object[0]._id;
@@ -48,9 +72,10 @@ export function setDataType(element, data, action) {
48
72
  el.setAttribute(data.type, data[data.type].name);
49
73
  }
50
74
  }
51
- setData(Array.from(elements.keys()), data);
52
75
  }
53
76
 
77
+ if (dataElements.size) await setData(Array.from(dataElements), data);
78
+
54
79
  if (action) {
55
80
  action.element.dispatchEvent(
56
81
  new CustomEvent("saved", {
@@ -107,14 +132,22 @@ export async function setData(element, data) {
107
132
  type = data.method.split(".")[0];
108
133
  } else if (type == "key") type = "object";
109
134
 
110
- if (data.$filter && data.$filter.query) {
111
- let sortedData = sort$in(data[type], data.$filter.query);
135
+ if (data.filter && data.filter.query) {
136
+ let sortedData = sort$in(data[type], data.filter.query);
112
137
  if (sortedData && sortedData.length) data[type] = sortedData;
113
138
  }
114
139
 
115
140
  for (let el of element) {
116
- // if rendered in server side skip
117
- if (el.hasAttribute("rendered")) return el.removeAttribute("rendered");
141
+ // Keep SSR content for this element without skipping its batch siblings.
142
+ if (el.hasAttribute("rendered")) {
143
+ el.removeAttribute("rendered");
144
+ continue;
145
+ }
146
+ if (
147
+ data.method?.endsWith(".delete") &&
148
+ !(el.getFilter || el.renderValue)
149
+ )
150
+ continue;
118
151
  if (el.hasAttribute("render-json")) {
119
152
  console.log("render-json", "");
120
153
  }
@@ -124,11 +157,24 @@ export async function setData(element, data) {
124
157
  if (action && ["database", "array", "object", "key"].includes(action))
125
158
  continue;
126
159
 
127
- if (data.array.includes("aiConversations")) {
128
- console.log("testing ai conversations");
160
+ const { key, isRead, isUpdate, isListen, isCrdt } = getAttributes(el);
161
+ if (key !== undefined && typeof key !== "string") {
162
+ console.error("[CoCreate elements] Ignoring element with a non-string key", {
163
+ key,
164
+ keyType: typeof key,
165
+ element: el
166
+ });
167
+ continue;
129
168
  }
130
169
 
131
- const { key, isRead, isUpdate, isListen, isCrdt } = getAttributes(el);
170
+ const hasRenderController =
171
+ el.hasAttribute("render-query") || el.hasAttribute("render-json");
172
+
173
+ // array/object without a key establishes CRUD identity only. Do not
174
+ // write the response into the element unless it explicitly controls a
175
+ // render target.
176
+ if (key === undefined && !hasRenderController) continue;
177
+
132
178
  let elementType = el.getAttribute("type");
133
179
  if (elementType !== "file" && (el.getFilter || el.renderValue))
134
180
  await filterData(el, data, type, key);
@@ -188,6 +234,24 @@ export async function setData(element, data) {
188
234
  }
189
235
 
190
236
  async function filterData(element, data, type, key) {
237
+ if (key === "$length" && (data.error || !Array.isArray(data[type]))) return;
238
+ if (
239
+ data.method &&
240
+ !data.method.endsWith(".read") &&
241
+ (element.getFilter || element.renderValue)
242
+ ) {
243
+ // A write payload is a document delta, not the result of this count's
244
+ // query. Keep the displayed count until its own filtered read completes.
245
+ // Re-read even for partial updates: a document may have left the query.
246
+ if (key === "$length" && element.getFilter && element.setFilter) {
247
+ if (data.error || !data[type]?.length) return;
248
+ element.setFilter(await element.getFilter());
249
+ return;
250
+ }
251
+ await checkFilters(element, data, type);
252
+ return;
253
+ }
254
+
191
255
  let operator = "",
192
256
  value;
193
257
 
@@ -250,24 +314,18 @@ async function filterData(element, data, type, key) {
250
314
  element.renderValue &&
251
315
  data.method &&
252
316
  data.method.endsWith(".read") &&
253
- data.$filter &&
254
- (data.$filter.overwrite || !isRendered)
317
+ data.filter &&
318
+ (data.filter.overwrite || !isRendered)
255
319
  ) {
256
320
  await element.renderValue(data);
257
321
  } else if (
258
- (data.$filter && data.$filter.loadmore) ||
322
+ (data.filter && data.filter.loadMore) ||
259
323
  (data.method && data.method.endsWith(".read") && isRendered)
260
324
  ) {
261
325
  await loadMore(element, data, type);
262
- } else if (
263
- element.getFilter &&
264
- data.method &&
265
- !data.method.endsWith(".read")
266
- ) {
267
- await checkFilters(element, data, type);
268
326
  } else if (element.renderValue) {
269
327
  if (key)
270
- data = {[key]: data}
328
+ data = { [key]: data }
271
329
  await element.renderValue(data);
272
330
  } else if (key === "$length") {
273
331
  element.setValue(data[type].length);
@@ -289,68 +347,84 @@ async function filterData(element, data, type, key) {
289
347
  value = data[type][0];
290
348
  } else if (data) element.setValue(data);
291
349
 
292
- if (data.$filter) {
350
+ if (data.filter) {
293
351
  let filterElement = filter.filters.get(element);
294
- if (filterElement) filterElement.index = data.$filter.index;
352
+ if (filterElement) filterElement.index = data.filter.index;
295
353
  }
296
354
 
297
- if (!data[type] || !Array.isArray(data[type]) || !data[type].length) return;
298
-
299
- const evt = new CustomEvent("fetchedData", { bubbles: true });
355
+ const evt = new CustomEvent("fetchedData", {
356
+ bubbles: true,
357
+ detail: data
358
+ });
300
359
  element.dispatchEvent(evt);
360
+
361
+ if (!data[type] || !Array.isArray(data[type]) || !data[type].length) return;
301
362
  }
302
363
 
303
364
  async function loadMore(element, data, type, sort) {
304
- let clonedData = { ...data };
305
- let renderedData = await element.getData();
306
- if (!renderedData || !renderedData[type]) {
307
- return;
308
- }
309
- for (let i = 0; i < clonedData[type].length; i++) {
310
- let index;
311
- if (type === "object") {
312
- index = renderedData[type].findIndex(
313
- (obj) => obj._id === clonedData[type][i]._id
314
- );
315
- } else {
316
- index = renderedData[type].findIndex(
317
- (obj) => obj.name === clonedData[type][i].name
318
- );
319
- }
320
-
321
- if (index >= 0) {
322
- clonedData[type].splice(i, 1);
323
- i--; // Adjust the index to account for the removed item
324
- }
325
- }
326
- if (clonedData[type].length > 0) {
327
- await element.renderValue(clonedData);
365
+ const renderedData = await element.getData();
366
+ if (!renderedData || !renderedData[type]) return;
367
+
368
+ const key = type === "object" ? "_id" : "name";
369
+ const renderedKeys = new Set(renderedData[type].map(item => item[key]));
370
+ const records = data[type].filter(item => {
371
+ if (renderedKeys.has(item[key])) return false;
372
+ renderedKeys.add(item[key]);
373
+ return true;
374
+ });
375
+ if (records.length) {
376
+ await element.renderValue({
377
+ ...data,
378
+ [type]: records,
379
+ filter: { ...data.filter, loadMore: true, overwrite: false }
380
+ });
328
381
  }
329
382
  }
330
383
 
331
384
  async function checkFilters(element, data, type) {
385
+ if (type && data[type]) {
386
+ const applyUpdate = (record) => {
387
+ if (!record?.$update) return record;
388
+ const { $update, ...fields } = record;
389
+ return { ...fields, ...$update };
390
+ };
391
+ data = {
392
+ ...data,
393
+ [type]: Array.isArray(data[type])
394
+ ? data[type].map(applyUpdate)
395
+ : applyUpdate(data[type])
396
+ };
397
+ }
398
+
332
399
  let localData;
333
400
  if (!element.getData) {
334
- // TODO: fix: getObject as this is related to render or form, but filter could exist on an input which does not have getObject
401
+ // TODO: getDataObject is related to render or form, but filter could exist on an input which does not have getData
335
402
  // TODO: generate an object of current element key: value to use with filter. because filter is used object is not defined
336
- localData = getObject(element);
337
- if (Array.isArray(localData[type])) localData = localData[type][0];
403
+ localData = getDataObject(element);
404
+ if (localData && Array.isArray(localData[type])) {
405
+ localData = localData[type][0];
406
+ }
338
407
  } else localData = await element.getData();
339
408
 
340
409
  if (!localData) return;
341
410
 
411
+ let filter = localData.filter
342
412
  let newData;
343
413
  if (type) {
344
414
  localData = localData[type];
345
415
  newData = data[type];
346
416
  } else newData = data;
347
417
 
348
- let filter = await element.getFilter();
349
- if (filter && filter.query) {
350
- newData = newData.filter(item => queryData(item, filter.query));
418
+ if (localData == null) {
419
+ if (element.renderValue) localData = [];
420
+ else return;
351
421
  }
352
422
 
353
- if (!newData || !newData.length) return;
423
+ if (
424
+ newData == null ||
425
+ (Array.isArray(newData) && newData.length === 0)
426
+ )
427
+ return;
354
428
 
355
429
  if (Array.isArray(localData)) {
356
430
  if (Array.isArray(newData)) {
@@ -361,6 +435,9 @@ async function checkFilters(element, data, type) {
361
435
  checkIndex(element, data, localData, newData, type, filter);
362
436
  }
363
437
  } else {
438
+ if (data.method?.endsWith(".delete")) return;
439
+
440
+ let matched = false;
364
441
  let primaryKey;
365
442
  if (type === "object") {
366
443
  primaryKey = "_id";
@@ -370,21 +447,27 @@ async function checkFilters(element, data, type) {
370
447
 
371
448
  if (Array.isArray(newData)) {
372
449
  for (let i = 0; i < newData.length; i++) {
450
+ if (!newData[i]) continue;
373
451
  if (
374
452
  typeof localData === "string" &&
375
453
  localData === newData[i][primaryKey]
376
454
  ) {
377
455
  localData = newData[i];
378
- } else if (localData[primaryKey] === newData[i][primaryKey]) {
456
+ matched = true;
457
+ } else if (localData[primaryKey] && localData[primaryKey] === newData[i][primaryKey]) {
379
458
  localData = dotNotationToObject(newData[i], localData);
459
+ matched = true;
380
460
  }
381
461
  }
382
462
  } else if (typeof localData === "string" && localData === newData[primaryKey]) {
383
463
  localData = newData;
384
- } else if (localData[primaryKey] === newData[primaryKey]) {
464
+ matched = true;
465
+ } else if (localData[primaryKey] && localData[primaryKey] === newData[primaryKey]) {
385
466
  localData = dotNotationToObject(newData, localData);
467
+ matched = true;
386
468
  }
387
469
 
470
+ if (!matched) return;
388
471
  if (element.renderValue) element.renderValue(data);
389
472
  // render({ element, data, key: type });
390
473
  else if (data) element.setValue(localData);
@@ -392,50 +475,51 @@ async function checkFilters(element, data, type) {
392
475
  }
393
476
 
394
477
  function checkIndex(element, data, localData, newData, type, filter) {
395
- let index;
396
- if (type === "object") {
397
- index = localData.findIndex((obj) => obj._id === newData._id);
398
- } else {
399
- index = localData.findIndex((obj) => obj.name === newData.name);
400
- }
401
-
402
- if (!data.$filter) data.$filter = {};
403
-
404
- if (data.method.endsWith(".delete")) {
478
+ if (!newData) return;
479
+ const primaryKey = type === "object" ? "_id" : "name";
480
+ const primaryValue = newData[primaryKey];
481
+ let index = localData.findIndex(
482
+ (item) => item?.[primaryKey] === primaryValue
483
+ );
484
+ const isMatch =
485
+ (!filter?.query || queryData(newData, filter.query)) &&
486
+ (!filter?.search || searchData(newData, filter.search));
487
+
488
+ if (!data.filter) data.filter = {};
489
+
490
+ if (data.method.endsWith(".delete") || !isMatch) {
405
491
  if (index === -1) return;
406
- data.$filter.remove = true;
492
+ data.filter.remove = true;
407
493
  localData.splice(index, 1);
408
494
  } else {
409
495
  if (data.method.endsWith(".create")) {
410
496
  if (index === -1) {
411
- data.$filter.create = true;
412
- if (filter && filter.sort) newData.isNewData = true;
413
- else index = localData.length;
497
+ data.filter.create = true;
498
+ index = localData.length;
414
499
  localData.push(newData);
415
500
  }
416
501
  } else {
417
502
  if (index === -1) return;
418
- data.$filter.update = true;
419
- data.$filter.currentIndex = index;
503
+ data.filter.update = true;
504
+ data.filter.currentIndex = index;
420
505
  localData[index] = dotNotationToObject(newData, localData[index]);
421
- if (filter && filter.sort) {
422
- localData[index].isNewData = true;
423
- }
424
506
  }
425
507
 
426
508
  if (filter && filter.sort) {
427
509
  localData = sortData(localData, filter.sort);
428
- index = localData.findIndex((obj) => obj.isNewData);
510
+ index = localData.findIndex(
511
+ (item) => item?.[primaryKey] === primaryValue
512
+ );
429
513
  }
430
514
  }
431
515
 
432
516
  if (index >= 0) {
433
- if (data.$filter.currentIndex === index)
434
- delete data.$filter.currentIndex;
435
- data.$filter.index = index;
517
+ if (data.filter.currentIndex === index)
518
+ delete data.filter.currentIndex;
519
+ data.filter.index = index;
436
520
  if (element.renderValue) element.renderValue(data);
437
521
  else if (data) element.setValue(localData);
438
522
  }
439
523
  }
440
524
 
441
- export default { setDataType, setData }
525
+ export default { setDataType, setData }