@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.
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +115 -12
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +36 -9
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue.js +10 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue.js.map +1 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js +305 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js.map +1 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js +38 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js.map +1 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +10 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js.map +1 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +398 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +155 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js +9 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js.map +1 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js +281 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js +137 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js +9 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js.map +1 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js +225 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js.map +1 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js +110 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +19 -3
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +513 -481
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +9 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js.map +1 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +181 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js +50 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +102 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -6
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js +9 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js +190 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js +91 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js +9 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js +64 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js +49 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js.map +1 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js +9 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js.map +1 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js +382 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js.map +1 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js +285 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js.map +1 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +9 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js.map +1 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js +277 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +183 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +16 -14
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +53 -22
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js +9 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js.map +1 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js +253 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js.map +1 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js +197 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js.map +1 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +106 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +105 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +510 -76
- package/dist/esm/utils/actionsDispatcher.js +11 -3
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/builtInModals.js +3 -1
- package/dist/esm/utils/builtInModals.js.map +1 -1
- package/dist/esm/utils/duplicateWarning.js +62 -0
- package/dist/esm/utils/duplicateWarning.js.map +1 -0
- package/dist/esm/utils/errors.js +4 -0
- package/dist/esm/utils/errors.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +4 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/openDuplicateWarning.js +39 -0
- package/dist/esm/utils/openDuplicateWarning.js.map +1 -0
- package/dist/esm/utils/optionsFrom.js +69 -2
- package/dist/esm/utils/optionsFrom.js.map +1 -1
- package/dist/esm/utils/planTree.js +57 -0
- package/dist/esm/utils/planTree.js.map +1 -0
- package/dist/esm/utils/redactionSegments.js +53 -0
- package/dist/esm/utils/redactionSegments.js.map +1 -0
- package/dist/esm/utils/registerNcDashboardWidgets.js +61 -0
- package/dist/esm/utils/registerNcDashboardWidgets.js.map +1 -0
- package/dist/esm/utils/searchRefusal.js +62 -0
- package/dist/esm/utils/searchRefusal.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +17560 -12844
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +510 -76
- package/l10n/en.json +106 -1
- package/l10n/nl.json +105 -1
- package/package.json +1 -1
- package/src/components/CnActionDialog/CnActionDialog.vue +134 -13
- package/src/components/CnAddressSearch/CnAddressSearch.vue +343 -0
- package/src/components/CnAddressSearch/index.js +12 -0
- package/src/components/CnAssignDialog/CnAssignDialog.vue +525 -0
- package/src/components/CnAssignDialog/index.js +12 -0
- package/src/components/CnDecisionListWidget/CnDecisionListWidget.vue +388 -0
- package/src/components/CnDecisionListWidget/index.js +12 -0
- package/src/components/CnDuplicateWarning/CnDuplicateWarning.vue +318 -0
- package/src/components/CnDuplicateWarning/index.js +12 -0
- package/src/components/CnFormDialog/CnFormDialog.vue +19 -3
- package/src/components/CnIndexPage/CnIndexPage.vue +52 -0
- package/src/components/CnNcDashboardList/CnNcDashboardList.vue +218 -0
- package/src/components/CnNcDashboardList/index.js +12 -0
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +107 -6
- package/src/components/CnPlanWidget/CnPlanWidget.vue +246 -0
- package/src/components/CnPlanWidget/CnPlanWidgetRow.vue +117 -0
- package/src/components/CnPlanWidget/index.js +12 -0
- package/src/components/CnRedactionEditor/CnRedactionEditor.vue +655 -0
- package/src/components/CnRedactionEditor/index.js +13 -0
- package/src/components/CnRetentionWidget/CnRetentionWidget.vue +413 -0
- package/src/components/CnRetentionWidget/index.js +12 -0
- package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +31 -0
- package/src/components/CnWorkQueue/CnWorkQueue.vue +463 -0
- package/src/components/CnWorkQueue/index.js +12 -0
- package/src/components/index.js +9 -0
- package/src/css/index-page.css +24 -0
- package/src/index.js +12 -0
- package/src/schemas/app-manifest-v2.schema.json +119 -3
- package/src/utils/actionsDispatcher.js +10 -2
- package/src/utils/builtInModals.js +3 -1
- package/src/utils/duplicateWarning.js +59 -0
- package/src/utils/errors.js +4 -0
- package/src/utils/libraryWidgetKeys.js +4 -0
- package/src/utils/openDuplicateWarning.js +35 -0
- package/src/utils/optionsFrom.js +68 -1
- package/src/utils/planTree.js +54 -0
- package/src/utils/redactionSegments.js +50 -0
- package/src/utils/registerNcDashboardWidgets.js +57 -0
- package/src/utils/searchRefusal.js +59 -0
- 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="
|
|
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="
|
|
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
|