@conduction/nextcloud-vue 2.108.0 → 2.110.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 (161) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +115 -12
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +36 -9
  5. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue.js +10 -0
  7. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue.js.map +1 -0
  8. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js +305 -0
  9. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js.map +1 -0
  10. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js +38 -0
  11. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js.map +1 -0
  12. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +10 -0
  13. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js.map +1 -0
  14. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +398 -0
  15. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -0
  16. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +155 -0
  17. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -0
  18. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js +9 -0
  19. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js.map +1 -0
  20. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js +281 -0
  21. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js.map +1 -0
  22. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js +137 -0
  23. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js.map +1 -0
  24. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js +9 -0
  25. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js.map +1 -0
  26. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js +225 -0
  27. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js.map +1 -0
  28. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js +110 -0
  29. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js.map +1 -0
  30. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  31. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +19 -3
  32. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  33. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -0
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  36. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +513 -481
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  38. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +9 -0
  39. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js.map +1 -0
  40. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +181 -0
  41. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -0
  42. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js +50 -0
  43. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -0
  44. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  45. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +102 -1
  46. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  47. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -6
  48. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  49. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js +9 -0
  50. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js.map +1 -0
  51. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js +190 -0
  52. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js.map +1 -0
  53. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js +91 -0
  54. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js.map +1 -0
  55. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js +9 -0
  56. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js.map +1 -0
  57. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js +64 -0
  58. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js.map +1 -0
  59. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js +49 -0
  60. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js.map +1 -0
  61. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js +9 -0
  62. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js.map +1 -0
  63. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js +382 -0
  64. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js.map +1 -0
  65. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js +285 -0
  66. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js.map +1 -0
  67. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +9 -0
  68. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js.map +1 -0
  69. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js +277 -0
  70. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -0
  71. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +183 -0
  72. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -0
  73. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +16 -14
  74. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  75. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +53 -22
  76. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  77. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js +9 -0
  78. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js.map +1 -0
  79. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js +253 -0
  80. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js.map +1 -0
  81. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js +197 -0
  82. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js.map +1 -0
  83. package/dist/esm/index.js +12 -0
  84. package/dist/esm/index.js.map +1 -1
  85. package/dist/esm/l10n/en.json.js +106 -1
  86. package/dist/esm/l10n/en.json.js.map +1 -1
  87. package/dist/esm/l10n/nl.json.js +105 -1
  88. package/dist/esm/l10n/nl.json.js.map +1 -1
  89. package/dist/esm/library-style.js +1 -1
  90. package/dist/esm/nextcloud-vue.css +510 -76
  91. package/dist/esm/utils/actionsDispatcher.js +11 -3
  92. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  93. package/dist/esm/utils/builtInModals.js +3 -1
  94. package/dist/esm/utils/builtInModals.js.map +1 -1
  95. package/dist/esm/utils/duplicateWarning.js +62 -0
  96. package/dist/esm/utils/duplicateWarning.js.map +1 -0
  97. package/dist/esm/utils/errors.js +4 -0
  98. package/dist/esm/utils/errors.js.map +1 -1
  99. package/dist/esm/utils/libraryWidgetKeys.js +4 -0
  100. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  101. package/dist/esm/utils/openDuplicateWarning.js +39 -0
  102. package/dist/esm/utils/openDuplicateWarning.js.map +1 -0
  103. package/dist/esm/utils/optionsFrom.js +69 -2
  104. package/dist/esm/utils/optionsFrom.js.map +1 -1
  105. package/dist/esm/utils/planTree.js +57 -0
  106. package/dist/esm/utils/planTree.js.map +1 -0
  107. package/dist/esm/utils/redactionSegments.js +53 -0
  108. package/dist/esm/utils/redactionSegments.js.map +1 -0
  109. package/dist/esm/utils/registerNcDashboardWidgets.js +61 -0
  110. package/dist/esm/utils/registerNcDashboardWidgets.js.map +1 -0
  111. package/dist/esm/utils/searchRefusal.js +62 -0
  112. package/dist/esm/utils/searchRefusal.js.map +1 -0
  113. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  114. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  115. package/dist/nextcloud-vue.cjs.js +17560 -12844
  116. package/dist/nextcloud-vue.cjs.js.map +2 -2
  117. package/dist/nextcloud-vue.css +510 -76
  118. package/l10n/en.json +106 -1
  119. package/l10n/nl.json +105 -1
  120. package/package.json +1 -1
  121. package/src/components/CnActionDialog/CnActionDialog.vue +134 -13
  122. package/src/components/CnAddressSearch/CnAddressSearch.vue +343 -0
  123. package/src/components/CnAddressSearch/index.js +12 -0
  124. package/src/components/CnAssignDialog/CnAssignDialog.vue +525 -0
  125. package/src/components/CnAssignDialog/index.js +12 -0
  126. package/src/components/CnDecisionListWidget/CnDecisionListWidget.vue +388 -0
  127. package/src/components/CnDecisionListWidget/index.js +12 -0
  128. package/src/components/CnDuplicateWarning/CnDuplicateWarning.vue +318 -0
  129. package/src/components/CnDuplicateWarning/index.js +12 -0
  130. package/src/components/CnFormDialog/CnFormDialog.vue +19 -3
  131. package/src/components/CnIndexPage/CnIndexPage.vue +52 -0
  132. package/src/components/CnNcDashboardList/CnNcDashboardList.vue +218 -0
  133. package/src/components/CnNcDashboardList/index.js +12 -0
  134. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +107 -6
  135. package/src/components/CnPlanWidget/CnPlanWidget.vue +246 -0
  136. package/src/components/CnPlanWidget/CnPlanWidgetRow.vue +117 -0
  137. package/src/components/CnPlanWidget/index.js +12 -0
  138. package/src/components/CnRedactionEditor/CnRedactionEditor.vue +655 -0
  139. package/src/components/CnRedactionEditor/index.js +13 -0
  140. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +413 -0
  141. package/src/components/CnRetentionWidget/index.js +12 -0
  142. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  143. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +31 -0
  144. package/src/components/CnWorkQueue/CnWorkQueue.vue +463 -0
  145. package/src/components/CnWorkQueue/index.js +12 -0
  146. package/src/components/index.js +9 -0
  147. package/src/css/index-page.css +24 -0
  148. package/src/index.js +12 -0
  149. package/src/schemas/app-manifest-v2.schema.json +119 -3
  150. package/src/utils/actionsDispatcher.js +10 -2
  151. package/src/utils/builtInModals.js +3 -1
  152. package/src/utils/duplicateWarning.js +59 -0
  153. package/src/utils/errors.js +4 -0
  154. package/src/utils/libraryWidgetKeys.js +4 -0
  155. package/src/utils/openDuplicateWarning.js +35 -0
  156. package/src/utils/optionsFrom.js +68 -1
  157. package/src/utils/planTree.js +54 -0
  158. package/src/utils/redactionSegments.js +50 -0
  159. package/src/utils/registerNcDashboardWidgets.js +57 -0
  160. package/src/utils/searchRefusal.js +59 -0
  161. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -0,0 +1,12 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnNcDashboardList — the compact list a Nextcloud Dashboard widget shows,
6
+ * read from OpenRegister or an endpoint.
7
+ */
8
+
9
+ import CnNcDashboardList from './CnNcDashboardList.vue'
10
+
11
+ export { CnNcDashboardList }
12
+ export default CnNcDashboardList
@@ -286,20 +286,20 @@
286
286
  data-testid="object-list-upload-input"
287
287
  @change="onUploadFilesSelected">
288
288
  <CnFormDialog
289
- v-if="showCreate && createSchema"
289
+ v-if="(showCreate || editingRow) && createSchema"
290
290
  ref="createDialog"
291
291
  :schema="createSchema"
292
- :item="null"
292
+ :item="editingRow"
293
293
  :register="content.register || ''"
294
- :initialData="createInitialData"
294
+ :initialData="editingRow ? null : createInitialData"
295
295
  :lockedFields="createLockedFields"
296
296
  :size="formSize"
297
297
  :columns="formColumns"
298
298
  :includeFields="formIncludeFields"
299
299
  :excludeFields="formExcludeFields"
300
300
  :fieldOverrides="formFieldOverrides"
301
- @confirm="onCreateConfirm"
302
- @close="showCreate = false" />
301
+ @confirm="editingRow ? onEditConfirm($event) : onCreateConfirm($event)"
302
+ @close="closeForm" />
303
303
  </div>
304
304
  </template>
305
305
 
@@ -320,6 +320,7 @@ import { objectDisplayName, objectFieldValue } from '../../utils/objectName.js'
320
320
  import { moveIndex, renumberRows, rowIdOf } from '../../utils/reorderRows.js'
321
321
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
322
322
  import { markNewTabHandled } from '../../utils/rowAuxClick.js'
323
+ import { changedKeys } from '../../utils/saveEdited.js'
323
324
  import { dispatchObjectCreated } from '../../utils/walkthroughSignals.js'
324
325
  import { CnRowActions } from '../CnRowActions/index.js'
325
326
 
@@ -458,7 +459,7 @@ export default {
458
459
  },
459
460
  },
460
461
 
461
- emits: ['created', 'row-click', 'row-aux-click', 'view-all', 'files-dropped'],
462
+ emits: ['created', 'updated', 'row-click', 'row-aux-click', 'view-all', 'files-dropped'],
462
463
 
463
464
  data() {
464
465
  return {
@@ -484,6 +485,8 @@ export default {
484
485
  fitStage: 0,
485
486
  /** Whether the create dialog is open. */
486
487
  showCreate: false,
488
+ /** The row the form edits (`content.editable`), or null while it creates. */
489
+ editingRow: null,
487
490
  /**
488
491
  * The list's own schema definition, once fetched.
489
492
  *
@@ -732,6 +735,12 @@ export default {
732
735
  ...this.rowActionLink(action),
733
736
  handler: (row) => this.runRowAction(action, row),
734
737
  }))
738
+ if (this.content.editable === true) {
739
+ mapped.unshift({ label: t('nextcloud-vue', 'Edit'), icon: 'Pencil', handler: (row) => this.openEdit(row) })
740
+ }
741
+ if (this.content.deletable === true) {
742
+ mapped.push({ label: t('nextcloud-vue', 'Delete'), icon: 'TrashCanOutline', destructive: true, handler: (row) => this.deleteRow(row) })
743
+ }
735
744
  if (this.reorderField === '') {
736
745
  return mapped
737
746
  }
@@ -1546,6 +1555,7 @@ export default {
1546
1555
  forgetCreateSchema() {
1547
1556
  this.createSchema = null
1548
1557
  this.showCreate = false
1558
+ this.editingRow = null
1549
1559
  },
1550
1560
 
1551
1561
  /**
@@ -1556,6 +1566,97 @@ export default {
1556
1566
  *
1557
1567
  * @return {Promise<void>}
1558
1568
  */
1569
+ /**
1570
+ * Edit a row in the list's own form (`content.editable`): the form the
1571
+ * Add button opens, filled with the row.
1572
+ *
1573
+ * @param {object} row The row.
1574
+ * @return {Promise<void>}
1575
+ * @spec openspec/changes/object-list-edit-in-place/specs/object-list-widget/spec.md#requirement-a-list-can-edit-a-row-in-its-own-form
1576
+ */
1577
+ async openEdit(row) {
1578
+ try {
1579
+ await this.ensureSchema()
1580
+ this.showCreate = false
1581
+ this.editingRow = row ? { ...row } : null
1582
+ } catch (e) {
1583
+ this.error = (e && e.message) || 'error'
1584
+ }
1585
+ },
1586
+
1587
+ /**
1588
+ * Delete a row of the list's own schema (`content.deletable`). The
1589
+ * toast offers Undo; a refusal toasts the server's message.
1590
+ *
1591
+ * @param {object} row The row.
1592
+ * @return {Promise<void>}
1593
+ * @spec openspec/changes/object-list-edit-in-place/specs/object-list-widget/spec.md#requirement-a-list-can-edit-a-row-in-its-own-form
1594
+ */
1595
+ async deleteRow(row) {
1596
+ const c = this.content || {}
1597
+ const id = rowIdOf(row)
1598
+ if (id === '') {
1599
+ return
1600
+ }
1601
+ const pending = (async () => {
1602
+ const [{ default: axios }, { generateUrl }] = await Promise.all([import('@nextcloud/axios'), import('@nextcloud/router')])
1603
+ try {
1604
+ await axios.delete(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}', { register: c.register, schema: c.schema, id }))
1605
+ } catch (e) {
1606
+ const data = e && e.response && e.response.data
1607
+ const refusal = data && (data.message || data.error)
1608
+ throw new Error((typeof refusal === 'string' && refusal) || (e && e.message) || t('nextcloud-vue', 'The delete failed'), { cause: e })
1609
+ }
1610
+ return true
1611
+ })()
1612
+ await this.reportDelete(row, pending)
1613
+ await pending.catch(() => {})
1614
+ this.fetchRows()
1615
+ },
1616
+
1617
+ /** Close the form, whether it created or edited. */
1618
+ closeForm() {
1619
+ this.showCreate = false
1620
+ this.editingRow = null
1621
+ },
1622
+
1623
+ /**
1624
+ * Save an edited row: a PATCH of the keys the form changed, so every
1625
+ * key it does not show keeps its stored value; nothing when nothing
1626
+ * changed. A refusal shows the server's message in the dialog.
1627
+ *
1628
+ * @param {object} formData The edited values.
1629
+ * @return {Promise<void>}
1630
+ * @spec openspec/changes/object-list-edit-in-place/specs/object-list-widget/spec.md#requirement-a-list-can-edit-a-row-in-its-own-form
1631
+ */
1632
+ async onEditConfirm(formData) {
1633
+ const c = this.content || {}
1634
+ const row = this.editingRow
1635
+ const id = rowIdOf(row)
1636
+ const changes = changedKeys(row, formData)
1637
+ try {
1638
+ if (id !== '' && Object.keys(changes).length > 0) {
1639
+ const [{ default: axios }, { generateUrl }] = await Promise.all([import('@nextcloud/axios'), import('@nextcloud/router')])
1640
+ await axios.patch(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}', { register: c.register, schema: c.schema, id }), changes)
1641
+ }
1642
+ if (this.$refs.createDialog) {
1643
+ this.$refs.createDialog.setResult({ success: true })
1644
+ }
1645
+ /**
1646
+ * @event updated Emitted after a row was edited, with the keys that changed.
1647
+ * @type {object}
1648
+ */
1649
+ this.$emit('updated', { id, changes })
1650
+ this.fetchRows()
1651
+ } catch (e) {
1652
+ const data = e && e.response && e.response.data
1653
+ const refusal = data && (data.message || data.error)
1654
+ if (this.$refs.createDialog) {
1655
+ this.$refs.createDialog.setResult({ error: (typeof refusal === 'string' && refusal) || (e && e.message) || 'error' })
1656
+ }
1657
+ }
1658
+ },
1659
+
1559
1660
  async openCreate() {
1560
1661
  const c = this.content || {}
1561
1662
  if (!c.schema) {
@@ -0,0 +1,246 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <div class="cn-plan" data-testid="cn-plan">
7
+ <NcLoadingIcon v-if="loading" :size="24" />
8
+ <NcEmptyContent v-else-if="error"
9
+ :name="t('nextcloud-vue', 'The plan is unavailable')"
10
+ :description="error"
11
+ data-testid="cn-plan-error">
12
+ <template #action>
13
+ <NcButton data-testid="cn-plan-retry" @click="load">
14
+ {{ t('nextcloud-vue', 'Try again') }}
15
+ </NcButton>
16
+ </template>
17
+ </NcEmptyContent>
18
+ <ul v-else-if="roots.length > 0" class="cn-plan__tree">
19
+ <li v-for="node in roots" :key="node.uuid" class="cn-plan__node">
20
+ <CnPlanWidgetRow :item="node"
21
+ :busy="busy"
22
+ @enable="enable"
23
+ @transition="transition" />
24
+ <ul v-if="node.children.length > 0" class="cn-plan__children">
25
+ <li v-for="child in node.children" :key="child.uuid" class="cn-plan__node">
26
+ <CnPlanWidgetRow :item="child"
27
+ :busy="busy"
28
+ @enable="enable"
29
+ @transition="transition" />
30
+ </li>
31
+ </ul>
32
+ </li>
33
+ </ul>
34
+ <p v-else class="cn-plan__empty" data-testid="cn-plan-empty">
35
+ {{ text(content.emptyText) || t('nextcloud-vue', 'This record has no plan') }}
36
+ </p>
37
+ </div>
38
+ </template>
39
+
40
+ <script>
41
+ import axios from '@nextcloud/axios'
42
+ import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
43
+ import { translate as t } from '@nextcloud/l10n'
44
+ import { generateUrl } from '@nextcloud/router'
45
+ import { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
46
+ import CnPlanWidgetRow from './CnPlanWidgetRow.vue'
47
+ import { planTree } from '../../utils/planTree.js'
48
+
49
+ /** The page refresh channel the built-in widgets listen on. */
50
+ const PAGE_REFRESH = 'cn:page:refresh'
51
+
52
+ /**
53
+ * CnPlanWidget — the `plan` detail widget: a record's planned steps (stages,
54
+ * tasks and milestones) as OpenRegister's case layer holds them, read from
55
+ * `/api/cases/{id}`, with the moves OpenRegister allows: start a
56
+ * discretionary item, complete or terminate one. It judges nothing:
57
+ * OpenRegister decides every move and its refusal is shown as it says it.
58
+ * A 404 means the record has no plan; any other failure is an error with a
59
+ * retry, never an empty plan. Any app whose records run on OpenRegister's
60
+ * case layer uses it; dossiq's case plan is the first.
61
+ *
62
+ * `content`: `emptyText`, through the host translate.
63
+ *
64
+ * The record's id comes from `objectId`, else the bound record.
65
+ */
66
+ export default {
67
+ name: 'CnPlanWidget',
68
+
69
+ components: { CnPlanWidgetRow, NcButton, NcEmptyContent, NcLoadingIcon },
70
+
71
+ inject: {
72
+ /** The host app's translate (CnAppRoot `cnTranslate`). */
73
+ cnTranslate: { default: () => (key) => key },
74
+ },
75
+
76
+ props: {
77
+ /**
78
+ * The widget's configuration: `{ emptyText? }`.
79
+ *
80
+ * @type {object}
81
+ */
82
+ content: {
83
+ type: Object,
84
+ default: () => ({}),
85
+ },
86
+
87
+ /**
88
+ * The bound record, on a detail page.
89
+ *
90
+ * @type {object|null}
91
+ */
92
+ objectData: {
93
+ type: Object,
94
+ default: null,
95
+ },
96
+
97
+ /** The record's id (else the bound record's). */
98
+ objectId: {
99
+ type: [String, Number],
100
+ default: '',
101
+ },
102
+ },
103
+
104
+ data() {
105
+ return {
106
+ loading: true,
107
+ busy: false,
108
+ error: '',
109
+ roots: [],
110
+ }
111
+ },
112
+
113
+ computed: {
114
+ /** @return {string} The record id. */
115
+ id() {
116
+ const bound = this.objectData || {}
117
+ return String(this.objectId || bound.id || (bound['@self'] && bound['@self'].id) || '')
118
+ },
119
+ },
120
+
121
+ mounted() {
122
+ subscribe(PAGE_REFRESH, this.load)
123
+ this.load()
124
+ },
125
+
126
+ beforeUnmount() {
127
+ unsubscribe(PAGE_REFRESH, this.load)
128
+ },
129
+
130
+ methods: {
131
+ t,
132
+
133
+ /**
134
+ * A config text through the host translate.
135
+ *
136
+ * @param {unknown} value The text.
137
+ * @return {string}
138
+ */
139
+ text(value) {
140
+ return typeof value === 'string' && value !== '' ? this.cnTranslate(value) : ''
141
+ },
142
+
143
+ /**
144
+ * The server's sentence for a failure.
145
+ *
146
+ * @param {unknown} failure The rejection.
147
+ * @return {string}
148
+ */
149
+ messageOf(failure) {
150
+ const response = failure && failure.response
151
+ if (response && response.status === 403) {
152
+ return t('nextcloud-vue', 'You are not allowed to see this plan.')
153
+ }
154
+ const body = response && response.data
155
+ const detail = body && (body.error || body.message)
156
+ return typeof detail === 'string' && detail !== '' ? detail : t('nextcloud-vue', 'The plan could not be loaded. Try again.')
157
+ },
158
+
159
+ /**
160
+ * Read the plan; 404 is "no plan", anything else an error.
161
+ *
162
+ * @spec openspec/changes/plan-widget/specs/plan-widget/spec.md#requirement-a-record-shows-its-planned-steps
163
+ * @return {Promise<void>}
164
+ */
165
+ async load() {
166
+ if (!this.id) {
167
+ this.loading = false
168
+ return
169
+ }
170
+ this.loading = true
171
+ this.error = ''
172
+ try {
173
+ const { data } = await axios.get(generateUrl(`/apps/openregister/api/cases/${encodeURIComponent(this.id)}`))
174
+ this.roots = data && Array.isArray(data.items) ? planTree(data.items) : []
175
+ } catch (failure) {
176
+ this.roots = []
177
+ if (!(failure && failure.response && failure.response.status === 404)) {
178
+ this.error = this.messageOf(failure)
179
+ }
180
+ } finally {
181
+ this.loading = false
182
+ }
183
+ },
184
+
185
+ /**
186
+ * Start a discretionary item.
187
+ *
188
+ * @param {object} item The plan item.
189
+ * @return {Promise<void>}
190
+ */
191
+ enable(item) {
192
+ return this.act(() => axios.post(generateUrl(`/apps/openregister/api/cases/items/${encodeURIComponent(item.uuid)}/enable`), {}))
193
+ },
194
+
195
+ /**
196
+ * Move an item to a target state.
197
+ *
198
+ * @param {object} item The plan item.
199
+ * @param {string} to The target state.
200
+ * @return {Promise<void>}
201
+ */
202
+ transition(item, to) {
203
+ return this.act(() => axios.post(generateUrl(`/apps/openregister/api/cases/items/${encodeURIComponent(item.uuid)}/transition`), { to, reason: '' }))
204
+ },
205
+
206
+ /**
207
+ * Run one move, then read the plan again: a move can cascade, so the
208
+ * next read is the plan, not the write's answer.
209
+ *
210
+ * @spec openspec/changes/plan-widget/specs/plan-widget/spec.md#requirement-a-record-shows-its-planned-steps
211
+ * @param {() => Promise<unknown>} write The move.
212
+ * @return {Promise<void>}
213
+ */
214
+ async act(write) {
215
+ this.busy = true
216
+ try {
217
+ await write()
218
+ await this.load()
219
+ emit(PAGE_REFRESH, {})
220
+ } catch (failure) {
221
+ this.error = this.messageOf(failure)
222
+ } finally {
223
+ this.busy = false
224
+ }
225
+ },
226
+ },
227
+ }
228
+ </script>
229
+
230
+ <style scoped>
231
+ .cn-plan__tree,
232
+ .cn-plan__children {
233
+ margin: 0;
234
+ padding: 0;
235
+ list-style: none;
236
+ }
237
+
238
+ .cn-plan__children {
239
+ padding-inline-start: 24px;
240
+ }
241
+
242
+ .cn-plan__empty {
243
+ margin: 0;
244
+ color: var(--color-text-maxcontrast);
245
+ }
246
+ </style>
@@ -0,0 +1,117 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <div class="cn-plan-row" :data-testid="`cn-plan-row-${item.key || item.uuid}`">
7
+ <span class="cn-plan-row__name">{{ item.name }}</span>
8
+ <span class="cn-plan-row__state" :data-state="item.state">{{ stateLabel(item.state) }}</span>
9
+ <NcButton v-if="item.state === 'enabled' && item.discretionary"
10
+ :disabled="busy"
11
+ :data-testid="`cn-plan-enable-${item.key || item.uuid}`"
12
+ @click="$emit('enable', item)">
13
+ {{ t('nextcloud-vue', 'Start') }}
14
+ </NcButton>
15
+ <NcButton v-for="target in targets"
16
+ :key="target"
17
+ :disabled="busy"
18
+ :data-testid="`cn-plan-${target}-${item.key || item.uuid}`"
19
+ @click="$emit('transition', item, target)">
20
+ {{ target === 'completed' ? t('nextcloud-vue', 'Complete') : t('nextcloud-vue', 'Stop') }}
21
+ </NcButton>
22
+ </div>
23
+ </template>
24
+
25
+ <script>
26
+ import { translate as t } from '@nextcloud/l10n'
27
+ import { NcButton } from '@nextcloud/vue'
28
+ import { offeredTransitions } from '../../utils/planTree.js'
29
+
30
+ /**
31
+ * One plan item in CnPlanWidget: its name, its state and the moves offered.
32
+ */
33
+ export default {
34
+ name: 'CnPlanWidgetRow',
35
+
36
+ components: { NcButton },
37
+
38
+ props: {
39
+ /**
40
+ * The plan item (`uuid`, `key`, `name`, `state`, `type`, `discretionary`).
41
+ *
42
+ * @type {object}
43
+ */
44
+ item: {
45
+ type: Object,
46
+ required: true,
47
+ },
48
+
49
+ /** A move is running. */
50
+ busy: {
51
+ type: Boolean,
52
+ default: false,
53
+ },
54
+ },
55
+
56
+ emits: ['enable', 'transition'],
57
+
58
+ computed: {
59
+ /** @return {string[]} The moves offered. */
60
+ targets() {
61
+ return offeredTransitions(this.item)
62
+ },
63
+ },
64
+
65
+ methods: {
66
+ t,
67
+
68
+ /**
69
+ * The word for a state.
70
+ *
71
+ * @param {string} state The state.
72
+ * @return {string}
73
+ */
74
+ stateLabel(state) {
75
+ const labels = {
76
+ available: t('nextcloud-vue', 'Waiting'),
77
+ enabled: t('nextcloud-vue', 'Can start'),
78
+ active: t('nextcloud-vue', 'In progress'),
79
+ completed: t('nextcloud-vue', 'Done'),
80
+ terminated: t('nextcloud-vue', 'Stopped'),
81
+ disabled: t('nextcloud-vue', 'Not needed'),
82
+ }
83
+ return labels[state] || String(state || '')
84
+ },
85
+ },
86
+ }
87
+ </script>
88
+
89
+ <style scoped>
90
+ .cn-plan-row {
91
+ display: flex;
92
+ flex-wrap: wrap;
93
+ align-items: center;
94
+ gap: 8px;
95
+ padding-block: 6px;
96
+ }
97
+
98
+ .cn-plan-row__name {
99
+ flex: 1 1 auto;
100
+ font-weight: 600;
101
+ }
102
+
103
+ .cn-plan-row__state {
104
+ padding: 2px 10px;
105
+ border-radius: 12px;
106
+ background: var(--color-background-dark);
107
+ font-size: 13px;
108
+ }
109
+
110
+ .cn-plan-row__state[data-state='active'] {
111
+ background: var(--color-primary-element-light);
112
+ }
113
+
114
+ .cn-plan-row__state[data-state='completed'] {
115
+ color: var(--color-success-text, var(--color-success));
116
+ }
117
+ </style>
@@ -0,0 +1,12 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnPlanWidget — the `plan` detail widget: a record's planned steps from
6
+ * OpenRegister's case layer, with the moves OpenRegister allows.
7
+ */
8
+
9
+ import CnPlanWidget from './CnPlanWidget.vue'
10
+
11
+ export { CnPlanWidget }
12
+ export default CnPlanWidget