@conduction/nextcloud-vue 2.75.0 → 2.76.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.
Files changed (67) hide show
  1. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +6 -0
  2. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  3. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -2
  4. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  5. package/dist/esm/components/CnCredentials/CnCredentials.vue2.js +11 -10
  6. package/dist/esm/components/CnCredentials/CnCredentials.vue2.js.map +1 -1
  7. package/dist/esm/components/CnCredentials/CnCredentials.vue3.js.map +1 -1
  8. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +8 -5
  9. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  10. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  11. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +144 -6
  12. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  13. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +12 -11
  14. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  15. package/dist/esm/components/CnIndexPage/selfModeActions.js +90 -14
  16. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  17. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +15 -3
  18. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  19. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +26 -0
  20. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  21. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +26 -9
  22. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  23. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +19 -3
  24. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  25. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  26. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +153 -38
  27. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  28. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +74 -56
  29. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  30. package/dist/esm/composables/useObjectLock.js +41 -10
  31. package/dist/esm/composables/useObjectLock.js.map +1 -1
  32. package/dist/esm/index.js +1 -0
  33. package/dist/esm/index.js.map +1 -1
  34. package/dist/esm/l10n/en.json.js +7 -1
  35. package/dist/esm/l10n/en.json.js.map +1 -1
  36. package/dist/esm/l10n/nl.json.js +8 -2
  37. package/dist/esm/l10n/nl.json.js.map +1 -1
  38. package/dist/esm/nextcloud-vue.css +88 -1
  39. package/dist/esm/store/useObjectStore.js +37 -9
  40. package/dist/esm/store/useObjectStore.js.map +1 -1
  41. package/dist/esm/utils/objectSignals.js +44 -0
  42. package/dist/esm/utils/objectSignals.js.map +1 -0
  43. package/dist/nextcloud-vue.cjs.js +871 -328
  44. package/dist/nextcloud-vue.cjs.js.map +2 -2
  45. package/dist/nextcloud-vue.css +88 -1
  46. package/l10n/en.json +7 -1
  47. package/l10n/nl.json +8 -2
  48. package/package.json +1 -1
  49. package/src/components/CnCardGrid/CnCardGrid.md +1 -0
  50. package/src/components/CnCardGrid/CnCardGrid.vue +7 -0
  51. package/src/components/CnCredentials/CnCredentials.vue +17 -16
  52. package/src/components/CnDetailPage/CnDetailPage.vue +8 -5
  53. package/src/components/CnIndexPage/CnIndexPage.md +15 -0
  54. package/src/components/CnIndexPage/CnIndexPage.vue +155 -16
  55. package/src/components/CnIndexPage/selfModeActions.js +89 -13
  56. package/src/components/CnIndexPage/useSelfFetchList.js +15 -3
  57. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  58. package/src/components/CnObjectCard/CnObjectCard.vue +89 -1
  59. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +19 -3
  60. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +181 -38
  61. package/src/composables/__tests__/useObjectLock.endpoints.spec.js +83 -0
  62. package/src/composables/useObjectLock.js +41 -10
  63. package/src/css/look-board-index.css +35 -1
  64. package/src/index.js +1 -0
  65. package/src/store/useObjectStore.js +37 -9
  66. package/src/utils/index.js +1 -0
  67. package/src/utils/objectSignals.js +41 -0
@@ -431,6 +431,12 @@ var script = {
431
431
  groups: [],
432
432
  /** Active tab key. */
433
433
  activeKey: '',
434
+ /** Keys of the tabbed sections whose request is still in flight (`objects`, `files`, `relations`). */
435
+ pendingSections: [],
436
+ /** Tabbed sections whose request failed: `{ key, label }`. */
437
+ failedSections: [],
438
+ /** Bumped per `loadTabs()`; an answer from an older load is dropped. */
439
+ loadToken: 0,
434
440
  /** Key of the inline-expanded row (`{group}-{id}`) for leaves with no owning-app page (e.g. notes). */
435
441
  expandedKey: '',
436
442
  /** Whether a footer file upload is in flight. */
@@ -555,6 +561,22 @@ var script = {
555
561
  )
556
562
  },
557
563
 
564
+ /**
565
+ * The line under the panel naming the sections still loading, once
566
+ * another section is already on screen.
567
+ *
568
+ * @return {string}
569
+ */
570
+ pendingLabel() {
571
+ const labels = {
572
+ objects: this.objectsLabel,
573
+ files: this.filesLabel,
574
+ relations: translate('nextcloud-vue', 'Linked items'),
575
+ };
576
+ const names = this.pendingSections.map((key) => labels[key] || key).join(', ');
577
+ return translate('nextcloud-vue', 'Still loading: {sections}', { sections: names })
578
+ },
579
+
558
580
  /** Placeholder shown in the body while the first fetch is in flight. */
559
581
  loadingLabel() {
560
582
  return translate('nextcloud-vue', 'Loading …')
@@ -1051,16 +1073,27 @@ var script = {
1051
1073
  * @return {Promise<object|null>}
1052
1074
  */
1053
1075
  async fetchSubResource(suffix) {
1076
+ return (await this.fetchSection(suffix)).data
1077
+ },
1078
+
1079
+ /**
1080
+ * GET an OpenRegister sub-resource and say whether it answered, so a
1081
+ * section can tell "nothing linked" from "could not load".
1082
+ *
1083
+ * @param {string} suffix - The sub-resource suffix.
1084
+ * @return {Promise<{ ok: boolean, data: object|null }>}
1085
+ */
1086
+ async fetchSection(suffix) {
1054
1087
  try {
1055
1088
  // `no-store`: relations change as the user links content; a stale
1056
1089
  // cached empty response would wrongly show the empty state on load.
1057
1090
  const response = await fetch(this.relatedUrl(suffix), { method: 'GET', headers: buildHeaders(), cache: 'no-store' });
1058
1091
  if (!response.ok) {
1059
- return null
1092
+ return { ok: false, data: null }
1060
1093
  }
1061
- return await response.json()
1094
+ return { ok: true, data: await response.json() }
1062
1095
  } catch {
1063
- return null
1096
+ return { ok: false, data: null }
1064
1097
  }
1065
1098
  },
1066
1099
 
@@ -1264,56 +1297,138 @@ var script = {
1264
1297
  },
1265
1298
 
1266
1299
  /**
1267
- * Fetch every group directly from OpenRegister and build the tabs.
1300
+ * Fetch every group directly from OpenRegister and build the tabs,
1301
+ * one section at a time.
1302
+ *
1303
+ * 🔴 EACH SECTION SHOWS WHEN ITS OWN REQUEST RETURNS. This used to wait
1304
+ * for relations, uses, used, contracts and files together behind one
1305
+ * `Promise.all`, so the relations call (0.45 s on pipelinq's lead page)
1306
+ * sat behind uses/used (about 9 s) and the card said "Loading …" the
1307
+ * whole time. Now the groups are laid out up front in their fixed order
1308
+ * and filled in place as each answer arrives, so a slow section never
1309
+ * reorders or hides a fast one. A section still in flight is named in
1310
+ * `pendingSections`; one whose request failed in `failedSections`.
1268
1311
  *
1312
+ * @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/related-objects-progressive/spec.md
1269
1313
  * @return {Promise<void>}
1270
1314
  */
1271
1315
  async loadTabs() {
1316
+ const token = ++this.loadToken;
1317
+ const current = () => token === this.loadToken;
1272
1318
  this.loading = true;
1273
- try {
1274
- const [relations, uses, used, contracts, files] = await Promise.all([
1275
- this.fetchSubResource('relations'),
1276
- this.showObjects ? this.fetchSubResource('uses') : null,
1277
- this.showObjects ? this.fetchSubResource('used') : null,
1278
- (this.showObjects && this.showContracts) ? this.fetchSubResource('contracts') : null,
1279
- this.showFiles ? this.fetchSubResource('files') : null,
1280
- ]);
1319
+ this.failedSections = [];
1320
+
1321
+ const objectSuffixes = this.showObjects
1322
+ ? ['uses', 'used', ...(this.showContracts ? ['contracts'] : [])]
1323
+ : [];
1324
+ const sections = [
1325
+ ...(this.showObjects ? [{ key: 'objects', label: this.objectsLabel }] : []),
1326
+ ...(this.showFiles ? [{ key: 'files', label: this.filesLabel }] : []),
1327
+ { key: 'relations', label: translate('nextcloud-vue', 'Linked items') },
1328
+ ];
1329
+ this.pendingSections = sections.map((section) => section.key);
1281
1330
 
1282
- const groups = [];
1331
+ // Lay out every group now, in its fixed order, empty. visibleGroups
1332
+ // hides an empty group, so nothing shows until a section fills it.
1333
+ const groups = [];
1334
+ if (this.showObjects) {
1335
+ groups.push({ key: 'objects', label: this.objectsLabel, icon: 'FileTreeOutline', integrationId: '', items: [], total: 0 });
1336
+ }
1337
+ if (this.showFiles) {
1338
+ groups.push({ key: 'files', label: this.filesLabel, icon: 'Paperclip', integrationId: 'files', items: [], total: 0 });
1339
+ }
1340
+ for (const def of LEAF_GROUPS) {
1341
+ groups.push({
1342
+ key: def.key,
1343
+ label: this.leafLabel(def.key),
1344
+ // Prefer the registered integration's icon so it matches the
1345
+ // sidebar/linked-apps; fall back to the known-good default.
1346
+ icon: this.integrationIcon(def.integrationId) || def.icon,
1347
+ integrationId: def.integrationId,
1348
+ requiredApp: def.requiredApp,
1349
+ items: [],
1350
+ total: 0,
1351
+ });
1352
+ }
1353
+ this.groups = groups;
1283
1354
 
1284
- if (this.showObjects) {
1285
- await this.resolveSchemaTitles([uses, used, contracts]);
1286
- const objectItems = this.mergeObjectResults([uses, used, contracts]);
1287
- groups.push({ key: 'objects', label: this.objectsLabel, icon: 'FileTreeOutline', integrationId: '', items: objectItems, total: objectItems.length });
1355
+ const setGroup = (key, items, total) => {
1356
+ const index = this.groups.findIndex((group) => group.key === key);
1357
+ if (index === -1) {
1358
+ return
1288
1359
  }
1289
-
1290
- if (this.showFiles) {
1291
- const fileResults = (files && files.results) || [];
1292
- groups.push({ key: 'files', label: this.filesLabel, icon: 'Paperclip', integrationId: 'files', items: fileResults.map((f) => this.toFileRow(f)), total: files ? (files.total ?? fileResults.length) : 0 });
1360
+ const next = this.groups.slice();
1361
+ next[index] = { ...next[index], items, total };
1362
+ this.groups = next;
1363
+ };
1364
+ const settle = (key, ok) => {
1365
+ this.pendingSections = this.pendingSections.filter((k) => k !== key);
1366
+ if (!ok) {
1367
+ const section = sections.find((s) => s.key === key);
1368
+ this.failedSections = [...this.failedSections, { key, label: section ? section.label : key }];
1369
+ }
1370
+ // Keep the tab the reader saw first: pin it, so a slower section
1371
+ // that sorts before it does not take the panel away.
1372
+ if (!this.activeKey && this.visibleGroups.length) {
1373
+ this.activeKey = this.visibleGroups[0].key;
1293
1374
  }
1375
+ };
1294
1376
 
1377
+ const loadRelations = this.fetchSection('relations').then(({ ok, data }) => {
1378
+ if (!current()) {
1379
+ return
1380
+ }
1295
1381
  for (const def of LEAF_GROUPS) {
1296
- const block = relations && relations[def.responseKey];
1382
+ const block = data && data[def.responseKey];
1297
1383
  const results = (block && block.results) || [];
1298
- groups.push({
1299
- key: def.key,
1300
- label: this.leafLabel(def.key),
1301
- // Prefer the registered integration's icon so it matches the
1302
- // sidebar/linked-apps; fall back to the known-good default.
1303
- icon: this.integrationIcon(def.integrationId) || def.icon,
1304
- integrationId: def.integrationId,
1305
- requiredApp: def.requiredApp,
1306
- items: results.map((r, i) => this.toLeafRow(r, i)),
1307
- total: block ? (block.total ?? results.length) : 0,
1308
- });
1384
+ setGroup(def.key, results.map((r, i) => this.toLeafRow(r, i)), block ? (block.total ?? results.length) : 0);
1309
1385
  }
1386
+ settle('relations', ok);
1387
+ });
1310
1388
 
1311
- this.groups = groups;
1312
- if (!this.activeGroup && this.visibleGroups.length) {
1313
- this.activeKey = this.visibleGroups[0].key;
1314
- }
1389
+ const loadFiles = this.showFiles
1390
+ ? this.fetchSection('files').then(({ ok, data }) => {
1391
+ if (!current()) {
1392
+ return
1393
+ }
1394
+ const fileResults = (data && data.results) || [];
1395
+ setGroup('files', fileResults.map((f) => this.toFileRow(f)), data ? (data.total ?? fileResults.length) : 0);
1396
+ settle('files', ok);
1397
+ })
1398
+ : null;
1399
+
1400
+ // uses, used and contracts merge into one Objects group: each answer
1401
+ // re-merges what has arrived so far, and the group stays pending
1402
+ // until the last of them is in.
1403
+ const envelopes = objectSuffixes.map(() => null);
1404
+ let objectsOk = true;
1405
+ const loadObjects = objectSuffixes.length
1406
+ ? Promise.all(objectSuffixes.map((suffix, i) => this.fetchSection(suffix).then(async ({ ok, data }) => {
1407
+ if (!current()) {
1408
+ return
1409
+ }
1410
+ objectsOk = objectsOk && ok;
1411
+ envelopes[i] = data;
1412
+ await this.resolveSchemaTitles([data]);
1413
+ if (!current()) {
1414
+ return
1415
+ }
1416
+ const objectItems = this.mergeObjectResults(envelopes);
1417
+ setGroup('objects', objectItems, objectItems.length);
1418
+ }))).then(() => {
1419
+ if (current()) {
1420
+ settle('objects', objectsOk);
1421
+ }
1422
+ })
1423
+ : null;
1424
+
1425
+ try {
1426
+ await Promise.all([loadRelations, loadFiles, loadObjects]);
1315
1427
  } finally {
1316
- this.loading = false;
1428
+ if (current()) {
1429
+ this.loading = false;
1430
+ this.pendingSections = [];
1431
+ }
1317
1432
  }
1318
1433
  },
1319
1434