@conduction/nextcloud-vue 2.74.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 (126) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +19 -14
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +2 -2
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +77 -26
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +22 -14
  8. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +23 -4
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  11. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  12. package/dist/esm/components/CnBrandBar/CnBrandBar.vue.js +9 -0
  13. package/dist/esm/components/CnBrandBar/CnBrandBar.vue.js.map +1 -0
  14. package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js +107 -0
  15. package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js.map +1 -0
  16. package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js +79 -0
  17. package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js.map +1 -0
  18. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +6 -0
  19. package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
  20. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -2
  21. package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
  22. package/dist/esm/components/CnCredentials/CnCredentials.vue2.js +11 -10
  23. package/dist/esm/components/CnCredentials/CnCredentials.vue2.js.map +1 -1
  24. package/dist/esm/components/CnCredentials/CnCredentials.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +8 -5
  26. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  28. package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js +4 -1
  29. package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue2.js.map +1 -1
  30. package/dist/esm/components/CnEnvironmentBanner/CnEnvironmentBanner.vue3.js.map +1 -1
  31. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -0
  32. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  33. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +14 -7
  34. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  35. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +32 -0
  36. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  37. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +6 -3
  38. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  39. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +144 -6
  40. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +14 -13
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/selfModeActions.js +90 -14
  44. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +15 -3
  46. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  47. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +26 -0
  48. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  49. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +26 -9
  50. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  51. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +19 -3
  52. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  53. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  54. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +18 -2
  55. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  56. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +2 -2
  57. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  58. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +153 -38
  59. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +74 -56
  61. package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +16 -0
  63. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
  64. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js +2 -2
  65. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
  66. package/dist/esm/composables/useObjectLock.js +41 -10
  67. package/dist/esm/composables/useObjectLock.js.map +1 -1
  68. package/dist/esm/directives/cnSelectAria.js +55 -0
  69. package/dist/esm/directives/cnSelectAria.js.map +1 -0
  70. package/dist/esm/index.js +1 -0
  71. package/dist/esm/index.js.map +1 -1
  72. package/dist/esm/l10n/en.json.js +7 -1
  73. package/dist/esm/l10n/en.json.js.map +1 -1
  74. package/dist/esm/l10n/nl.json.js +8 -2
  75. package/dist/esm/l10n/nl.json.js.map +1 -1
  76. package/dist/esm/nextcloud-vue.css +154 -2
  77. package/dist/esm/store/useObjectStore.js +37 -9
  78. package/dist/esm/store/useObjectStore.js.map +1 -1
  79. package/dist/esm/utils/cnFetch.js +17 -2
  80. package/dist/esm/utils/cnFetch.js.map +1 -1
  81. package/dist/esm/utils/objectSignals.js +44 -0
  82. package/dist/esm/utils/objectSignals.js.map +1 -0
  83. package/dist/esm/utils/resolveBrand.js +38 -0
  84. package/dist/esm/utils/resolveBrand.js.map +1 -0
  85. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  86. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  87. package/dist/nextcloud-vue.cjs.js +9640 -8649
  88. package/dist/nextcloud-vue.cjs.js.map +2 -2
  89. package/dist/nextcloud-vue.css +154 -2
  90. package/l10n/en.json +7 -1
  91. package/l10n/nl.json +8 -2
  92. package/package.json +1 -1
  93. package/src/components/CnAppNav/CnAppNav.vue +21 -16
  94. package/src/components/CnAppRoot/CnAppRoot.vue +53 -0
  95. package/src/components/CnBannerWidget/CnBannerWidget.vue +23 -4
  96. package/src/components/CnBrandBar/CnBrandBar.vue +230 -0
  97. package/src/components/CnCardGrid/CnCardGrid.md +1 -0
  98. package/src/components/CnCardGrid/CnCardGrid.vue +7 -0
  99. package/src/components/CnCredentials/CnCredentials.vue +17 -16
  100. package/src/components/CnDetailPage/CnDetailPage.vue +8 -5
  101. package/src/components/CnEnvironmentBanner/CnEnvironmentBanner.vue +4 -1
  102. package/src/components/CnFormDialog/CnFormDialog.vue +10 -0
  103. package/src/components/CnFormPage/CnFormPage.vue +33 -0
  104. package/src/components/CnIndexPage/CnIndexPage.md +15 -0
  105. package/src/components/CnIndexPage/CnIndexPage.vue +157 -18
  106. package/src/components/CnIndexPage/selfModeActions.js +89 -13
  107. package/src/components/CnIndexPage/useSelfFetchList.js +15 -3
  108. package/src/components/CnObjectCard/CnObjectCard.md +1 -0
  109. package/src/components/CnObjectCard/CnObjectCard.vue +89 -1
  110. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +19 -3
  111. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +20 -4
  112. package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +181 -38
  113. package/src/components/CnStatsBlock/CnStatsBlock.vue +18 -2
  114. package/src/composables/__tests__/useObjectLock.endpoints.spec.js +83 -0
  115. package/src/composables/useObjectLock.js +41 -10
  116. package/src/css/form-field.css +1 -1
  117. package/src/css/look-board-index.css +35 -1
  118. package/src/directives/cnSelectAria.js +54 -0
  119. package/src/index.js +1 -0
  120. package/src/schemas/app-manifest-v2.schema.json +6 -1
  121. package/src/store/useObjectStore.js +37 -9
  122. package/src/utils/cnFetch.js +16 -1
  123. package/src/utils/index.js +1 -0
  124. package/src/utils/objectSignals.js +41 -0
  125. package/src/utils/resolveBrand.js +37 -0
  126. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -82,7 +82,7 @@
82
82
  :activeFilterChips="activeFilterChips"
83
83
  :showBuildiqButton="!buildiqInHeader"
84
84
  :bulkNoun="boardBulkNoun"
85
- :bulkHint="bulkHint"
85
+ :bulkHint="bulkHint ? cnTranslate(bulkHint) : bulkHint"
86
86
  :pagination="effectivePagination"
87
87
  :objectCount="effectiveObjects.length"
88
88
  :selectable="selectable"
@@ -430,19 +430,19 @@
430
430
  name="form-dialog"
431
431
  :show="showFormDialogVisible"
432
432
  :item="editItem"
433
- :schema="effectiveSchema"
433
+ :schema="formSchema"
434
434
  :confirm="onFormConfirm"
435
435
  :close="closeFormDialog"
436
436
  :refresh="onRefreshEvent">
437
437
  <CnFormDialog
438
438
  v-if="showFormDialogVisible && !useAdvancedFormDialog"
439
439
  ref="formDialog"
440
- :schema="effectiveSchema"
440
+ :schema="formSchema"
441
441
  :item="editItem"
442
- :register="register"
443
- :excludeFields="excludeFields"
444
- :includeFields="includeFields"
445
- :fieldOverrides="fieldOverrides"
442
+ :register="formRegister"
443
+ :excludeFields="rowFormTarget ? [] : excludeFields"
444
+ :includeFields="rowFormTarget ? null : includeFields"
445
+ :fieldOverrides="rowFormTarget ? {} : fieldOverrides"
446
446
  :nameField="massActionNameField"
447
447
  :size="formSize"
448
448
  :columns="formColumns"
@@ -456,11 +456,11 @@
456
456
  <CnAdvancedFormDialog
457
457
  v-if="showFormDialogVisible && useAdvancedFormDialog"
458
458
  ref="formDialog"
459
- :schema="effectiveSchema"
459
+ :schema="formSchema"
460
460
  :item="editItem"
461
- :excludeFields="excludeFields"
462
- :includeFields="includeFields"
463
- :fieldOverrides="fieldOverrides"
461
+ :excludeFields="rowFormTarget ? [] : excludeFields"
462
+ :includeFields="rowFormTarget ? null : includeFields"
463
+ :fieldOverrides="rowFormTarget ? {} : fieldOverrides"
464
464
  :nameField="massActionNameField"
465
465
  :initialValues="resolvedCreateDefaults"
466
466
  @confirm="onFormConfirm"
@@ -788,6 +788,7 @@
788
788
  :selectable="selectable"
789
789
  :clickToView="rowClickOpens"
790
790
  :cardFields="resolvedCardFields"
791
+ :accentOf="cardAccent"
791
792
  :selectedIds="internalSelectedIds"
792
793
  :rowKey="rowKey"
793
794
  :emptyText="emptyText"
@@ -848,7 +849,7 @@
848
849
  <CnPagination
849
850
  v-if="effectivePagination && (effectivePagination.pages > 1 || (isBoardLook && effectivePagination.total > 0))"
850
851
  :variant="isBoardLook ? 'board' : ''"
851
- :footerNote="footerNote"
852
+ :footerNote="footerNote ? cnTranslate(footerNote) : footerNote"
852
853
  :currentPage="effectivePagination.page || 1"
853
854
  :totalPages="effectivePagination.pages || 1"
854
855
  :totalItems="effectivePagination.total || 0"
@@ -971,6 +972,7 @@ import { openRowTarget } from '../../utils/linkNavigation.js'
971
972
  import { resolveClaimedTeams, resolveClaimTokens, splitViewsIntoTabs, viewAsTab, viewIdOf } from '../../utils/listLenses.js'
972
973
  import { LIST_SHORTCUTS, listPaletteCommands, shortcutFor } from '../../utils/listShortcuts.js'
973
974
  import { multiKeySort } from '../../utils/multiKeySort.js'
975
+ import { OBJECTS_CHANGED_EVENT } from '../../utils/objectSignals.js'
974
976
  import { withPersonalLenses } from '../../utils/personalLenses.js'
975
977
  import { resolveDeepTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'
976
978
  import { resolveRowActions } from '../../utils/resolveRowActions.js'
@@ -1016,7 +1018,7 @@ import { buildDefaultActions } from './defaultActions.js'
1016
1018
  import { dispatchAction } from './manifestActionDispatch.js'
1017
1019
  import { applyManualOrder, dropInOrder, manualOrderKey, moveInOrder, visibleIdsOf } from './manualOrder.js'
1018
1020
  import { orderColumns, personalColumnsKey, reconcilePersonalColumns } from './personalColumns.js'
1019
- import { createSelfModeActions } from './selfModeActions.js'
1021
+ import { createSelfModeActions, resolveRowTarget } from './selfModeActions.js'
1020
1022
  import { applyRowPatches, normalisePaneWidth, rowIdOf, splitLayoutFor } from './splitView.js'
1021
1023
  import { useNamedSource } from './useNamedSource.js'
1022
1024
  import { useSelfFetchList } from './useSelfFetchList.js'
@@ -1475,6 +1477,17 @@ export default {
1475
1477
  default: null,
1476
1478
  },
1477
1479
 
1480
+ /**
1481
+ * Gives each card in the card view a status accent: a colored start
1482
+ * border and icon. See CnObjectCard `accent`.
1483
+ *
1484
+ * @type {((object: object) => ({variant: string, icon: string, label: string}|null))|null}
1485
+ */
1486
+ cardAccent: {
1487
+ type: Function,
1488
+ default: null,
1489
+ },
1490
+
1478
1491
  /**
1479
1492
  * Board look: free text after the footer's count ("8 of 48 · click a
1480
1493
  * column header to sort"). Manifest key `config.footerNote`.
@@ -1646,6 +1659,21 @@ export default {
1646
1659
  default: () => [],
1647
1660
  },
1648
1661
 
1662
+ /**
1663
+ * Self-fetch only: list from this endpoint instead of
1664
+ * `/api/objects/{register}/{schema}`, e.g. an app endpoint that searches
1665
+ * several register/schema pairs at once. `register` and `schema` stay the
1666
+ * page's own pair (columns, create, export, import). Rows of another pair,
1667
+ * read from their `@self.register` / `@self.schema`, are edited, copied and
1668
+ * deleted against that pair, with its own form. The endpoint takes
1669
+ * `_page`, `_limit`, `_search` and `_order[key]=dir` and answers
1670
+ * `{ results, total, page, pages }`.
1671
+ */
1672
+ collectionUrl: {
1673
+ type: String,
1674
+ default: '',
1675
+ },
1676
+
1649
1677
  /**
1650
1678
  * Offer an "Also search inside files" switch beside the search box. On,
1651
1679
  * a search also matches words inside attached files (OpenRegister
@@ -3153,6 +3181,8 @@ export default {
3153
3181
  */
3154
3182
  persistedFilterKeys: [...(this.initialQueryFilterKeys || [])],
3155
3183
  internalSelectedIds: [...this.selectedIds],
3184
+ // Rows seen in a collectionUrl list, so a selection kept across pages still resolves its own pair.
3185
+ collectionRowsById: markRaw(new Map()),
3156
3186
  // Folder-sidebar state: selected folder id + the register-fetched list.
3157
3187
  selectedFolderId: null,
3158
3188
  /**
@@ -3190,6 +3220,9 @@ export default {
3190
3220
  // Dialog targets
3191
3221
  actionTargetItem: null,
3192
3222
  editItem: null,
3223
+ // The pair and schema of a row edited outside the page's own pair (collectionUrl).
3224
+ rowFormTarget: null,
3225
+ rowFormSchema: null,
3193
3226
  // Drives the Actions-menu Refresh spinner during a self-fetch
3194
3227
  // refresh, where the host has no promise to bind `:refreshing` to.
3195
3228
  internalRefreshing: false,
@@ -4471,6 +4504,16 @@ export default {
4471
4504
  return (this.schema && typeof this.schema === 'object') ? this.schema : null
4472
4505
  },
4473
4506
 
4507
+ /** The form dialog's schema: the edited row's own when it is of another pair. */
4508
+ formSchema() {
4509
+ return this.rowFormTarget ? this.rowFormSchema : this.effectiveSchema
4510
+ },
4511
+
4512
+ /** The form dialog's register, following `formSchema`. */
4513
+ formRegister() {
4514
+ return this.rowFormTarget ? this.rowFormTarget.register : this.register
4515
+ },
4516
+
4474
4517
  /**
4475
4518
  * Schema slug for the export-leaf URL — the `schema` prop directly
4476
4519
  * when it's a string (self-fetch mode's precondition), else the
@@ -4857,8 +4900,7 @@ export default {
4857
4900
  this.$emit('edit-open', row)
4858
4901
  return
4859
4902
  }
4860
- this.editItem = row
4861
- this.showFormDialogVisible = true
4903
+ this.openFormFor(row)
4862
4904
  },
4863
4905
  onCopy: (row) => {
4864
4906
  this.actionTargetItem = row
@@ -5367,6 +5409,21 @@ export default {
5367
5409
  },
5368
5410
 
5369
5411
  watch: {
5412
+ effectiveObjects: {
5413
+ immediate: true,
5414
+ handler(rows) {
5415
+ if (!this.collectionUrl || !this.isSelfFetchMode || !Array.isArray(rows)) {
5416
+ return
5417
+ }
5418
+ for (const row of rows) {
5419
+ const id = row?.id ?? row?.['@self']?.id
5420
+ if (id !== undefined && id !== null) {
5421
+ this.collectionRowsById.set(id, row)
5422
+ }
5423
+ }
5424
+ },
5425
+ },
5426
+
5370
5427
  // A column or form field bound to a property the schema lacks is told to the host once.
5371
5428
  effectiveSchema: {
5372
5429
  immediate: true,
@@ -5559,6 +5616,9 @@ export default {
5559
5616
  if (this.splitViewEnabled && typeof window !== 'undefined') {
5560
5617
  window.addEventListener('resize', this.measureSplitViewport)
5561
5618
  }
5619
+ if (typeof window !== 'undefined') {
5620
+ window.addEventListener(OBJECTS_CHANGED_EVENT, this.onObjectsChanged)
5621
+ }
5562
5622
  this.loadManualOrder()
5563
5623
  this.loadPersonalColumns()
5564
5624
  this.publishHoistedSidebar()
@@ -5583,6 +5643,8 @@ export default {
5583
5643
  this.selfActions = createSelfModeActions({
5584
5644
  isSelfFetchMode: () => this.isSelfFetchMode,
5585
5645
  selfObjectStore: () => this.selfObjectStore,
5646
+ rowTarget: (row) => this.rowTypeTarget(row),
5647
+ knownRow: (id) => this.collectionRowsById.get(id),
5586
5648
  selfObjectType: () => this.selfObjectType,
5587
5649
  list: () => this.list,
5588
5650
  selectedIds: () => this.internalSelectedIds,
@@ -5608,13 +5670,14 @@ export default {
5608
5670
  },
5609
5671
 
5610
5672
  /**
5611
- * Drop the resize listener and the hoisted sidebar.
5673
+ * Drop the window listeners and the hoisted sidebar.
5612
5674
  *
5613
5675
  * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md
5614
5676
  */
5615
5677
  beforeUnmount() {
5616
5678
  if (typeof window !== 'undefined') {
5617
5679
  window.removeEventListener('resize', this.measureSplitViewport)
5680
+ window.removeEventListener(OBJECTS_CHANGED_EVENT, this.onObjectsChanged)
5618
5681
  }
5619
5682
  // Clear the holder so the hoisted sidebar disappears when
5620
5683
  // the user navigates away from the index page.
@@ -7076,6 +7139,33 @@ export default {
7076
7139
  }
7077
7140
  },
7078
7141
 
7142
+ /**
7143
+ * Refresh the list when `dispatchObjectsChanged` names this page's
7144
+ * register and schema, by slug or id. A part missing on either side matches.
7145
+ *
7146
+ * @param {CustomEvent} event The objects-changed event.
7147
+ * @return {void}
7148
+ */
7149
+ onObjectsChanged(event) {
7150
+ // A collection list can hold rows of any pair, so any change may show in it.
7151
+ if (this.isSelfFetchMode && this.collectionUrl) {
7152
+ this.onRefreshEvent()
7153
+ return
7154
+ }
7155
+ const detail = (event && event.detail) || {}
7156
+ const schema = this.effectiveSchema
7157
+ const schemaIds = [this.schema, schema && schema.id, schema && schema.slug]
7158
+ .filter((v) => v !== undefined && v !== null && v !== '' && typeof v !== 'object')
7159
+ .map(String)
7160
+ if (detail.register && this.register && detail.register !== String(this.register)) {
7161
+ return
7162
+ }
7163
+ if (detail.schema && schemaIds.length > 0 && !schemaIds.includes(detail.schema)) {
7164
+ return
7165
+ }
7166
+ this.onRefreshEvent()
7167
+ },
7168
+
7079
7169
  /** @return {Promise<void>} */
7080
7170
  async onRefreshEvent() {
7081
7171
  this.$emit('refresh')
@@ -8218,6 +8308,8 @@ export default {
8218
8308
  closeFormDialog() {
8219
8309
  this.showFormDialogVisible = false
8220
8310
  this.editItem = null
8311
+ this.rowFormTarget = null
8312
+ this.rowFormSchema = null
8221
8313
  },
8222
8314
 
8223
8315
  /**
@@ -8278,8 +8370,55 @@ export default {
8278
8370
  * @public
8279
8371
  */
8280
8372
  openFormDialog(item = null) {
8281
- this.editItem = item
8282
- this.showFormDialogVisible = true
8373
+ return this.openFormFor(item)
8374
+ },
8375
+
8376
+ /**
8377
+ * Open the form dialog for an item. A row of another pair (collectionUrl)
8378
+ * opens with its own schema; everything else opens at once, as before.
8379
+ *
8380
+ * @param {object|null} item The row to edit, or null to create.
8381
+ * @return {Promise<void>|void}
8382
+ */
8383
+ openFormFor(item) {
8384
+ const target = item ? this.rowTypeTarget(item) : null
8385
+ if (!target) {
8386
+ this.rowFormTarget = null
8387
+ this.rowFormSchema = null
8388
+ this.editItem = item
8389
+ this.showFormDialogVisible = true
8390
+ return
8391
+ }
8392
+ const store = this.selfObjectStore
8393
+ return Promise.resolve(store.getSchema?.(target.type) || store.fetchSchema?.(target.type))
8394
+ .catch(() => null)
8395
+ .then((schema) => {
8396
+ if (!schema) {
8397
+ this.toastSavedView('error', t('nextcloud-vue', 'Could not load the form for this item.'))
8398
+ return
8399
+ }
8400
+ this.rowFormTarget = target
8401
+ this.rowFormSchema = schema
8402
+ this.editItem = item
8403
+ this.showFormDialogVisible = true
8404
+ })
8405
+ },
8406
+
8407
+ /**
8408
+ * The row's own pair when it differs from the page's in a collectionUrl list.
8409
+ *
8410
+ * @param {object} row The row.
8411
+ * @return {{register: string, schema: string, type: string}|null} The pair, or null.
8412
+ */
8413
+ rowTypeTarget(row) {
8414
+ return resolveRowTarget({
8415
+ collectionUrl: this.isSelfFetchMode ? this.collectionUrl : '',
8416
+ register: this.register,
8417
+ schema: this.schema,
8418
+ primarySchema: this.effectiveSchema,
8419
+ primaryRegister: this.selfObjectStore?.getRegister?.(this.selfObjectType) || null,
8420
+ store: this.selfObjectStore,
8421
+ }, row)
8283
8422
  },
8284
8423
 
8285
8424
  /**
@@ -13,6 +13,58 @@ function resolveNameField(ctx) {
13
13
 
14
14
  function findSource(ctx, id) {
15
15
  return ctx.effectiveObjects().find((o) => o.id === id || o['@self']?.id === id)
16
+ || (typeof ctx.knownRow === 'function' ? ctx.knownRow(id) : undefined)
17
+ }
18
+
19
+ /**
20
+ * The register/schema of a row that belongs to another pair than the page's,
21
+ * in a list fetched from a `collectionUrl`. Registers a store type for that
22
+ * pair on first use. Returns null for rows of the page's own pair, and always
23
+ * without a `collectionUrl`.
24
+ *
25
+ * @param {object} opts Page context.
26
+ * @param {string} opts.collectionUrl The page's collection endpoint, or ''.
27
+ * @param {string} opts.register The page's register.
28
+ * @param {string} opts.schema The page's schema.
29
+ * @param {object|null} opts.primarySchema The page's resolved schema.
30
+ * @param {object|null} [opts.primaryRegister] The page's resolved register, which matches a slug `register` to `@self.register`.
31
+ * @param {object|null} opts.store The object store.
32
+ * @param {object} row The row.
33
+ * @return {{register: string, schema: string, type: string}|null} The row's own pair.
34
+ */
35
+ export function resolveRowTarget({ collectionUrl, register, schema, primarySchema, primaryRegister, store }, row) {
36
+ if (!collectionUrl || !row || !store) {
37
+ return null
38
+ }
39
+ const self = row['@self'] || {}
40
+ const reg = self.register !== undefined && self.register !== null ? String(self.register) : ''
41
+ const sch = self.schema !== undefined && self.schema !== null ? String(self.schema) : ''
42
+ if (!reg || !sch) {
43
+ return null
44
+ }
45
+ const idsOf = (...values) => values.filter((v) => v !== undefined && v !== null && v !== '').map(String)
46
+ const schemaIds = idsOf(schema, primarySchema && primarySchema.id, primarySchema && primarySchema.slug)
47
+ const registerIds = idsOf(register, primaryRegister && primaryRegister.id, primaryRegister && primaryRegister.slug)
48
+ if (schemaIds.includes(sch) && registerIds.includes(reg)) {
49
+ return null
50
+ }
51
+ const type = `${reg}-${sch}`
52
+ if (!store.objectTypeRegistry?.[type]) {
53
+ store.registerObjectType(type, sch, reg, { registerSlug: reg, schemaSlug: sch })
54
+ }
55
+ return { register: reg, schema: sch, type }
56
+ }
57
+
58
+ /**
59
+ * The store type a row is written through: its own pair's, or the page's.
60
+ *
61
+ * @param {object} ctx Accessor closures.
62
+ * @param {object} [row] The row.
63
+ * @return {string} The type slug.
64
+ */
65
+ function typeFor(ctx, row) {
66
+ const target = row && typeof ctx.rowTarget === 'function' ? ctx.rowTarget(row) : null
67
+ return target ? target.type : ctx.selfObjectType()
16
68
  }
17
69
 
18
70
  /**
@@ -25,6 +77,10 @@ function findSource(ctx, id) {
25
77
  */
26
78
  function copyAddress(ctx, source) {
27
79
  const self = source['@self'] || {}
80
+ const target = typeof ctx.rowTarget === 'function' ? ctx.rowTarget(source) : null
81
+ if (target) {
82
+ return { register: target.register, schema: target.schema, id: String(source.id || self.id) }
83
+ }
28
84
  const reg = ctx.register()
29
85
  const sch = ctx.schema()
30
86
  const register = (typeof reg === 'string' && reg) || self.register
@@ -54,12 +110,12 @@ function refreshList(ctx) {
54
110
  }
55
111
  }
56
112
 
57
- function storeError(ctx) {
58
- return ctx.selfObjectStore()?.getError?.(ctx.selfObjectType())
113
+ function storeError(ctx, type = ctx.selfObjectType()) {
114
+ return ctx.selfObjectStore()?.getError?.(type)
59
115
  }
60
116
 
61
- function storeErrorMessage(ctx, fallback) {
62
- const err = storeError(ctx)
117
+ function storeErrorMessage(ctx, fallback, type) {
118
+ const err = storeError(ctx, type)
63
119
  return (err && err.message) || fallback
64
120
  }
65
121
 
@@ -76,13 +132,14 @@ export function createSelfModeActions(ctx) {
76
132
  return false
77
133
  }
78
134
  try {
79
- const ok = await ctx.selfObjectStore().deleteObject(ctx.selfObjectType(), id)
135
+ const type = typeFor(ctx, findSource(ctx, id))
136
+ const ok = await ctx.selfObjectStore().deleteObject(type, id)
80
137
  if (ok) {
81
138
  ctx.setResults.singleDelete({ success: true })
82
139
  ctx.emit('delete', id)
83
140
  refreshList(ctx)
84
141
  } else {
85
- ctx.setResults.singleDelete({ error: storeErrorMessage(ctx, 'Delete failed') })
142
+ ctx.setResults.singleDelete({ error: storeErrorMessage(ctx, 'Delete failed', type) })
86
143
  }
87
144
  } catch (err) {
88
145
  ctx.setResults.singleDelete({ error: (err && err.message) || 'Delete failed' })
@@ -95,12 +152,28 @@ export function createSelfModeActions(ctx) {
95
152
  return false
96
153
  }
97
154
  try {
98
- const { successfulIds, failedIds } = await ctx.selfObjectStore().deleteObjects(ctx.selfObjectType(), ids)
155
+ // One request per pair: a mixed list holds rows of several types.
156
+ const idsByType = new Map()
157
+ for (const id of ids) {
158
+ const type = typeFor(ctx, findSource(ctx, id))
159
+ idsByType.set(type, [...(idsByType.get(type) || []), id])
160
+ }
161
+ const successfulIds = []
162
+ const failedIds = []
163
+ let failedType = null
164
+ for (const [type, typeIds] of idsByType) {
165
+ const outcome = await ctx.selfObjectStore().deleteObjects(type, typeIds)
166
+ successfulIds.push(...outcome.successfulIds)
167
+ failedIds.push(...outcome.failedIds)
168
+ if (outcome.failedIds.length > 0 && failedType === null) {
169
+ failedType = type
170
+ }
171
+ }
99
172
  if (failedIds.length === 0) {
100
173
  ctx.setResults.massDelete({ success: true, successfulIds })
101
174
  } else {
102
175
  ctx.setResults.massDelete({
103
- error: storeErrorMessage(ctx, `Failed to delete ${failedIds.length} item(s)`),
176
+ error: storeErrorMessage(ctx, `Failed to delete ${failedIds.length} item(s)`, failedType),
104
177
  successfulIds,
105
178
  failedIds,
106
179
  })
@@ -142,13 +215,14 @@ export function createSelfModeActions(ctx) {
142
215
  }
143
216
  }
144
217
  const clone = cloneObjectForCopy(source, newName, resolveNameField(ctx))
145
- const saved = await ctx.selfObjectStore().saveObject(ctx.selfObjectType(), clone)
218
+ const type = typeFor(ctx, source)
219
+ const saved = await ctx.selfObjectStore().saveObject(type, clone)
146
220
  if (saved) {
147
221
  ctx.setResults.singleCopy({ success: true })
148
222
  ctx.emit('copy', payload)
149
223
  refreshList(ctx)
150
224
  } else {
151
- ctx.setResults.singleCopy({ error: storeErrorMessage(ctx, 'Copy failed') })
225
+ ctx.setResults.singleCopy({ error: storeErrorMessage(ctx, 'Copy failed', type) })
152
226
  }
153
227
  } catch (err) {
154
228
  ctx.setResults.singleCopy({ error: (err && err.message) || 'Copy failed' })
@@ -191,7 +265,7 @@ export function createSelfModeActions(ctx) {
191
265
  }
192
266
  const clone = cloneObjectForCopy(source, getName(source), nameField)
193
267
  try {
194
- const saved = await ctx.selfObjectStore().saveObject(ctx.selfObjectType(), clone)
268
+ const saved = await ctx.selfObjectStore().saveObject(typeFor(ctx, source), clone)
195
269
  if (saved) {
196
270
  successfulIds.push(id)
197
271
  } else {
@@ -261,7 +335,9 @@ export function createSelfModeActions(ctx) {
261
335
  return false
262
336
  }
263
337
  try {
264
- const saved = await ctx.selfObjectStore().saveObject(ctx.selfObjectType(), formData)
338
+ const item = ctx.editItem()
339
+ const type = item ? typeFor(ctx, item) : ctx.selfObjectType()
340
+ const saved = await ctx.selfObjectStore().saveObject(type, formData)
265
341
  if (saved) {
266
342
  ctx.setResults.form({ success: true })
267
343
  const isCreate = !ctx.editItem()
@@ -277,7 +353,7 @@ export function createSelfModeActions(ctx) {
277
353
  ctx.afterCreateSuccess(saved)
278
354
  }
279
355
  } else {
280
- const err = storeError(ctx)
356
+ const err = storeError(ctx, type)
281
357
  if (err && err.isValidation) {
282
358
  // Keep the form visible so the user can fix the invalid data.
283
359
  ctx.setResults.formValidation(err.fields, err.message || 'Validation failed')
@@ -106,7 +106,11 @@ export function useSelfFetchList(props, instance, inject, extras = {}) {
106
106
  const register = folder.register || props.register
107
107
  return { register, schema: folder.schema, type: `${register}-${folder.schema}` }
108
108
  }
109
- return { register: props.register, schema: props.schema, type: `${props.register}-${props.schema}` }
109
+ // A collection endpoint gets its own type key, so a plain registration of
110
+ // the same pair elsewhere never resets it.
111
+ const collectionUrl = props.collectionUrl || ''
112
+ const type = `${props.register}-${props.schema}` + (collectionUrl ? `@${collectionUrl}` : '')
113
+ return { register: props.register, schema: props.schema, type, collectionUrl }
110
114
  })
111
115
  const objectType = computed(() => resolvedTarget.value.type)
112
116
  const sidebarState = inject('sidebarState', null) ?? inject('objectSidebarState', null)
@@ -155,9 +159,17 @@ export function useSelfFetchList(props, instance, inject, extras = {}) {
155
159
  // second arg) — that previously made fetch URLs go to `/api/objects/undefined/[object Object]`.
156
160
  // Runs (sync, ahead of the list's own watcher) again whenever the type switches.
157
161
  function registerTarget() {
158
- const { register, schema, type } = resolvedTarget.value
162
+ const { register, schema, type, collectionUrl } = resolvedTarget.value
159
163
  if (typeof objectStore.registerObjectType === 'function') {
160
- objectStore.registerObjectType(type, schema, register, { registerSlug: register, schemaSlug: schema })
164
+ objectStore.registerObjectType(type, schema, register, {
165
+ registerSlug: register,
166
+ schemaSlug: schema,
167
+ ...(collectionUrl ? { collectionUrl } : {}),
168
+ })
169
+ }
170
+ // A mixed list tells its own rows apart by `@self.register`, an id, so a slug register needs resolving.
171
+ if (collectionUrl && !/^\d+$/.test(String(register)) && typeof objectStore.fetchRegister === 'function') {
172
+ objectStore.fetchRegister(type)
161
173
  }
162
174
  }
163
175
  registerTarget()
@@ -137,6 +137,7 @@ export default {
137
137
  | Prop | Type | Default | Description |
138
138
  |---|---|---|---|
139
139
  | `maxMetadata` | `Number` | `4` | Maximum number of schema properties to show in the metadata section. Properties are sorted by their `order` value before slicing |
140
+ | `accent` | `Object` | `null` | Status accent `{ variant, icon?, label? }`: a colored start border and, with `icon`, a colored icon before the title. `variant` is `success`, `warning`, `error`, `info` or `primary`; `label` is the icon's tooltip and accessible name |
140
141
 
141
142
  ## Slots
142
143
 
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div
3
3
  class="cn-object-card"
4
- :class="{ 'cn-object-card--selected': selected, 'cn-object-card--board': isBoardLook }"
4
+ :class="[{ 'cn-object-card--selected': selected, 'cn-object-card--board': isBoardLook }, accentVariant && ['cn-object-card--accent', `cn-object-card--accent-${accentVariant}`]]"
5
5
  @mousedown="onCardMouseDown"
6
6
  @click="onCardClick($event)"
7
7
  @auxclick="onCardAuxClick($event)">
@@ -42,6 +42,16 @@
42
42
  to report. A card whose consumer passes no badges
43
43
  would otherwise show no padlock at all. -->
44
44
  <CnLockIndicator :object="object" :size="16" />
45
+ <span
46
+ v-if="accentVariant && accent.icon"
47
+ class="cn-object-card__accent-icon"
48
+ data-testid="cn-object-card-accent-icon"
49
+ :role="accent.label ? 'img' : null"
50
+ :aria-label="accent.label || null"
51
+ :aria-hidden="accent.label ? null : 'true'"
52
+ :title="accent.label || null">
53
+ <CnIcon :name="accent.icon" :size="16" />
54
+ </span>
45
55
  {{ title }}
46
56
  </h3>
47
57
  <p v-if="description" class="cn-object-card__description">
@@ -128,6 +138,8 @@ import { CnIcon } from '../CnIcon/index.js'
128
138
  import { CnLockIndicator } from '../CnLockIndicator/index.js'
129
139
  import { CnStatusBadge } from '../CnStatusBadge/index.js'
130
140
 
141
+ const ACCENT_VARIANTS = ['success', 'warning', 'error', 'info', 'primary']
142
+
131
143
  /**
132
144
  * CnObjectCard — Schema-configuration-driven card for object display.
133
145
  *
@@ -244,6 +256,20 @@ export default {
244
256
  default: null,
245
257
  },
246
258
 
259
+ /**
260
+ * A status accent: a colored border at the start of the card and,
261
+ * with `icon`, a colored icon before the title. `variant` is one of
262
+ * `success`, `warning`, `error`, `info` or `primary`; `icon` is a
263
+ * name registered with CnIcon; `label` names the status for screen
264
+ * readers and as the icon's tooltip.
265
+ *
266
+ * @type {{variant: string, icon?: string, label?: string}}
267
+ */
268
+ accent: {
269
+ type: Object,
270
+ default: null,
271
+ },
272
+
247
273
  /**
248
274
  * The property keys the facts list shows, in order (manifest key
249
275
  * `config.cardFields`). Without it the card shows the first
@@ -307,6 +333,16 @@ export default {
307
333
  return null
308
334
  },
309
335
 
336
+ /**
337
+ * The accent variant when it is a known one, else ''.
338
+ *
339
+ * @return {string}
340
+ */
341
+ accentVariant() {
342
+ const variant = this.accent?.variant
343
+ return ACCENT_VARIANTS.includes(variant) ? variant : ''
344
+ },
345
+
310
346
  config() {
311
347
  return this.schema?.configuration || {}
312
348
  },
@@ -502,6 +538,58 @@ export default {
502
538
  background: var(--color-primary-element-light);
503
539
  }
504
540
 
541
+ /* The 4px start border takes 3px of padding, so the content stays put. */
542
+ .cn-object-card.cn-object-card--accent {
543
+ border-inline-start-width: 4px;
544
+ padding-inline-start: 13px;
545
+ }
546
+
547
+ .cn-object-card.cn-object-card--accent-success {
548
+ border-inline-start-color: var(--color-element-success);
549
+ }
550
+
551
+ .cn-object-card.cn-object-card--accent-warning {
552
+ border-inline-start-color: var(--color-element-warning);
553
+ }
554
+
555
+ .cn-object-card.cn-object-card--accent-error {
556
+ border-inline-start-color: var(--color-element-error);
557
+ }
558
+
559
+ .cn-object-card.cn-object-card--accent-info {
560
+ border-inline-start-color: var(--color-element-info);
561
+ }
562
+
563
+ .cn-object-card.cn-object-card--accent-primary {
564
+ border-inline-start-color: var(--color-primary-element);
565
+ }
566
+
567
+ .cn-object-card__accent-icon {
568
+ display: inline-flex;
569
+ vertical-align: -2px;
570
+ margin-inline-end: 4px;
571
+ }
572
+
573
+ .cn-object-card--accent-success .cn-object-card__accent-icon {
574
+ color: var(--color-text-success);
575
+ }
576
+
577
+ .cn-object-card--accent-warning .cn-object-card__accent-icon {
578
+ color: var(--color-text-warning);
579
+ }
580
+
581
+ .cn-object-card--accent-error .cn-object-card__accent-icon {
582
+ color: var(--color-text-error);
583
+ }
584
+
585
+ .cn-object-card--accent-info .cn-object-card__accent-icon {
586
+ color: var(--color-element-info);
587
+ }
588
+
589
+ .cn-object-card--accent-primary .cn-object-card__accent-icon {
590
+ color: var(--color-primary-element);
591
+ }
592
+
505
593
  .cn-object-card__checkbox {
506
594
  flex-shrink: 0;
507
595
  padding-top: 2px;