@conduction/nextcloud-vue 2.115.0 → 2.116.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/CnActionButtons/CnActionButtons.vue.js +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +92 -0
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +2 -2
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue.js +9 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue2.js +145 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue3.js +58 -0
- package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChecklistWidget/index.js +28 -0
- package/dist/esm/components/CnChecklistWidget/index.js.map +1 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +25 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +376 -51
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +364 -285
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +6 -4
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +1 -1
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +48 -1
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +11 -2
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue.js +1 -1
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js +11 -3
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js +8 -3
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +55 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +94 -4
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +248 -239
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +9 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +282 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js +202 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -0
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +10 -5
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +64 -0
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useTaskInboxStore.js +85 -0
- package/dist/esm/composables/useTaskInboxStore.js.map +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js +9 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js.map +1 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js +107 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js.map +1 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js +110 -0
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js.map +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +19 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +23 -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 +1054 -911
- package/dist/esm/utils/actionsDispatcher.js +10 -5
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/detailActionModel.js +6 -2
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetDispatch.js +1 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +17078 -15224
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1054 -911
- package/l10n/en.json +19 -1
- package/l10n/nl.json +23 -1
- package/package.json +1 -1
- package/src/components/CnActionButtons/CnActionButtons.vue +93 -1
- package/src/components/CnChecklistWidget/CnChecklistWidget.vue +206 -0
- package/src/components/CnChecklistWidget/index.js +27 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +26 -1
- package/src/components/CnDetailPage/CnDetailPage.md +2 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +377 -10
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +15 -2
- package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +59 -1
- package/src/components/CnMacrosWidget/CnMacrosWidget.vue +16 -6
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +55 -2
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +101 -3
- package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +508 -0
- package/src/components/CnTabsWidget/CnTabsWidget.vue +10 -5
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/index.js +1 -0
- package/src/composables/indexSources.js +64 -0
- package/src/composables/useTaskInboxStore.js +85 -0
- package/src/dialogs/CnAccessPurposeDialog.vue +183 -0
- package/src/index.js +1 -0
- package/src/schemas/app-manifest-v2.schema.json +49 -3
- package/src/utils/actionsDispatcher.js +10 -5
- package/src/utils/detailActionModel.js +6 -2
- package/src/utils/libraryWidgetKeys.js +1 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/widgetDispatch.js +1 -1
package/l10n/en.json
CHANGED
|
@@ -1932,7 +1932,25 @@
|
|
|
1932
1932
|
"no": "no",
|
|
1933
1933
|
"Recompute nomination": "Recompute nomination",
|
|
1934
1934
|
"Recompute": "Recompute",
|
|
1935
|
-
"Task deadline": "Deadline"
|
|
1935
|
+
"Task deadline": "Deadline",
|
|
1936
|
+
"The note could not be recorded. It is kept here; try again.": "The note could not be recorded. It is kept here; try again.",
|
|
1937
|
+
"This field is not available here.": "This field is not available here.",
|
|
1938
|
+
"filled in, not recorded yet": "filled in, not recorded yet",
|
|
1939
|
+
"Why are you opening this?": "Why are you opening this?",
|
|
1940
|
+
"This record holds special categories of personal data. Your purpose is recorded with your name and the time.": "This record holds special categories of personal data. Your purpose is recorded with your name and the time.",
|
|
1941
|
+
"Purpose": "Purpose",
|
|
1942
|
+
"Describe your purpose": "Describe your purpose",
|
|
1943
|
+
"State a purpose to open this": "State a purpose to open this",
|
|
1944
|
+
"This record holds special categories of personal data. It opens once your purpose is recorded.": "This record holds special categories of personal data. It opens once your purpose is recorded.",
|
|
1945
|
+
"State a purpose": "State a purpose",
|
|
1946
|
+
"This page cannot record a purpose, so the record stays closed.": "This page cannot record a purpose, so the record stays closed.",
|
|
1947
|
+
"Your purpose could not be recorded, so the record stays closed.": "Your purpose could not be recorded, so the record stays closed.",
|
|
1948
|
+
"Hand back": "Hand back",
|
|
1949
|
+
"Pick up": "Pick up",
|
|
1950
|
+
"Task completed": "Task completed",
|
|
1951
|
+
"This could not be done. Try again.": "This could not be done. Try again.",
|
|
1952
|
+
"{done} of {total} done": "{done} of {total} done",
|
|
1953
|
+
"Nothing to check off": "Nothing to check off"
|
|
1936
1954
|
},
|
|
1937
1955
|
"plurals": {
|
|
1938
1956
|
"{count} member": [
|
package/l10n/nl.json
CHANGED
|
@@ -2149,7 +2149,25 @@
|
|
|
2149
2149
|
"Last run: {name}, {date}": "Laatst uitgevoerd: {name}, {date}",
|
|
2150
2150
|
"This record has no macros": "Er zijn geen macro's voor dit item",
|
|
2151
2151
|
"Running …": "Bezig …",
|
|
2152
|
-
"Execute": "Uitvoeren"
|
|
2152
|
+
"Execute": "Uitvoeren",
|
|
2153
|
+
"The note could not be recorded. It is kept here; try again.": "De notitie kon niet worden vastgelegd. Ze blijft hier staan; probeer het opnieuw.",
|
|
2154
|
+
"This field is not available here.": "Dit veld is hier niet beschikbaar.",
|
|
2155
|
+
"filled in, not recorded yet": "ingevuld, nog niet vastgelegd",
|
|
2156
|
+
"Why are you opening this?": "Waarom open je dit?",
|
|
2157
|
+
"This record holds special categories of personal data. Your purpose is recorded with your name and the time.": "Hierin staan bijzondere persoonsgegevens. Je doel wordt vastgelegd met je naam en het tijdstip.",
|
|
2158
|
+
"Purpose": "Doel",
|
|
2159
|
+
"Describe your purpose": "Beschrijf je doel",
|
|
2160
|
+
"State a purpose to open this": "Geef een doel op om dit te openen",
|
|
2161
|
+
"This record holds special categories of personal data. It opens once your purpose is recorded.": "Hierin staan bijzondere persoonsgegevens. Het opent zodra je doel is vastgelegd.",
|
|
2162
|
+
"State a purpose": "Doel opgeven",
|
|
2163
|
+
"This page cannot record a purpose, so the record stays closed.": "Deze pagina kan geen doel vastleggen, dus het blijft gesloten.",
|
|
2164
|
+
"Your purpose could not be recorded, so the record stays closed.": "Je doel kon niet worden vastgelegd, dus het blijft gesloten.",
|
|
2165
|
+
"Hand back": "Teruggeven",
|
|
2166
|
+
"Pick up": "Oppakken",
|
|
2167
|
+
"Task completed": "Taak afgerond",
|
|
2168
|
+
"This could not be done. Try again.": "Dit lukte niet. Probeer het opnieuw.",
|
|
2169
|
+
"{done} of {total} done": "{done} van {total} klaar",
|
|
2170
|
+
"Nothing to check off": "Niets af te vinken"
|
|
2153
2171
|
},
|
|
2154
2172
|
"plurals": {
|
|
2155
2173
|
"{count} member": [
|
|
@@ -2279,6 +2297,10 @@
|
|
|
2279
2297
|
"%n passage found. Review each one.": [
|
|
2280
2298
|
"%n passage gevonden. Beoordeel elke passage.",
|
|
2281
2299
|
"%n passages gevonden. Beoordeel elke passage."
|
|
2300
|
+
],
|
|
2301
|
+
"{count} change: {list}": [
|
|
2302
|
+
"{count} wijziging: {list}",
|
|
2303
|
+
"{count} wijzigingen: {list}"
|
|
2282
2304
|
]
|
|
2283
2305
|
}
|
|
2284
2306
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.116.0",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
v-if="formEntry && formSchema && !formEntry.advanced"
|
|
172
172
|
ref="formDialog"
|
|
173
173
|
:schema="formSchema"
|
|
174
|
-
:item="
|
|
174
|
+
:item="formItem"
|
|
175
175
|
:register="formRegister"
|
|
176
176
|
:initialData="formInitialValues || {}"
|
|
177
177
|
:dialogTitle="tr(formEntry.formTitle) || ''"
|
|
@@ -247,6 +247,11 @@ import { CnIcon } from '../CnIcon/index.js'
|
|
|
247
247
|
* `excludeFields` / `fieldOverrides` narrow what the button asks for, which
|
|
248
248
|
* is how one schema serves both a full editor and a quick-create button.
|
|
249
249
|
* `advanced: true` swaps in the properties/JSON table for a power user.
|
|
250
|
+
* With `endpoint: { url, method?, loadUrl?, responsePath? }` and
|
|
251
|
+
* `formProperties` (JSON-schema properties) it edits a record that is not a
|
|
252
|
+
* register object instead: it reads the record from the endpoint, shows the
|
|
253
|
+
* declared fields filled in, and sends them back with PATCH (or PUT). With
|
|
254
|
+
* `method: "POST"` it creates one: an empty form, posted to the endpoint.
|
|
250
255
|
* - **`toggle`** — a two-way state button: `GET`s `stateSource` on mount,
|
|
251
256
|
* renders `labelOn` / `labelOff`, and on click `writes` the flipped
|
|
252
257
|
* value OPTIMISTICALLY, reverting on failure.
|
|
@@ -428,6 +433,8 @@ export default {
|
|
|
428
433
|
formEntry: null,
|
|
429
434
|
/** The fetched schema object for the open-form dialog (null until loaded). */
|
|
430
435
|
formSchema: null,
|
|
436
|
+
/** The record an endpoint form edits (null for a create form). */
|
|
437
|
+
formItem: null,
|
|
431
438
|
/** The run-node action currently in flight / showing its dialog (null = closed). */
|
|
432
439
|
runNodeEntry: null,
|
|
433
440
|
/** The target node's declared config-form fields, from the describe GET. */
|
|
@@ -993,6 +1000,11 @@ export default {
|
|
|
993
1000
|
async openForm(entry) {
|
|
994
1001
|
this.formEntry = entry
|
|
995
1002
|
this.formSchema = null
|
|
1003
|
+
this.formItem = null
|
|
1004
|
+
if (entry.endpoint && typeof entry.endpoint.url === 'string' && entry.endpoint.url !== '') {
|
|
1005
|
+
await this.openEndpointForm(entry)
|
|
1006
|
+
return
|
|
1007
|
+
}
|
|
996
1008
|
const register = entry.register || this.objectCtx.register || ''
|
|
997
1009
|
const schema = entry.schema || ''
|
|
998
1010
|
if (!schema) {
|
|
@@ -1015,6 +1027,81 @@ export default {
|
|
|
1015
1027
|
}
|
|
1016
1028
|
},
|
|
1017
1029
|
|
|
1030
|
+
/**
|
|
1031
|
+
* Open an `open-form` action with an `endpoint` as an edit form over a
|
|
1032
|
+
* record that is not a register object (an OpenRegister organisation):
|
|
1033
|
+
* the fields are the action's `formProperties`, the values are read from
|
|
1034
|
+
* `endpoint.loadUrl` (else `endpoint.url`) at `endpoint.responsePath`,
|
|
1035
|
+
* and the save sends the changed fields to `endpoint.url`. A failed
|
|
1036
|
+
* read toasts and opens nothing.
|
|
1037
|
+
*
|
|
1038
|
+
* @param {object} entry The open-form action with `endpoint` and `formProperties`.
|
|
1039
|
+
* @return {Promise<void>}
|
|
1040
|
+
* @spec openspec/changes/open-form-endpoint/specs/open-form-endpoint/spec.md#requirement-an-open-form-action-can-edit-a-record-through-an-endpoint
|
|
1041
|
+
*/
|
|
1042
|
+
async openEndpointForm(entry) {
|
|
1043
|
+
const properties = entry.formProperties && typeof entry.formProperties === 'object' && !Array.isArray(entry.formProperties) ? entry.formProperties : {}
|
|
1044
|
+
if (String(entry.endpoint.method || '').toUpperCase() === 'POST') {
|
|
1045
|
+
this.formSchema = { type: 'object', properties }
|
|
1046
|
+
return
|
|
1047
|
+
}
|
|
1048
|
+
const loadUrl = entry.endpoint.loadUrl || entry.endpoint.url
|
|
1049
|
+
const record = await fetchEndpointSource({ url: loadUrl, responsePath: entry.endpoint.responsePath || '' }, this.tokenCtx, { force: true })
|
|
1050
|
+
.catch(() => null)
|
|
1051
|
+
if (!record || typeof record !== 'object') {
|
|
1052
|
+
const { showError } = await import('@nextcloud/dialogs')
|
|
1053
|
+
if (typeof showError === 'function') {
|
|
1054
|
+
showError(t('nextcloud-vue', 'Could not open the form.'))
|
|
1055
|
+
}
|
|
1056
|
+
this.formEntry = null
|
|
1057
|
+
return
|
|
1058
|
+
}
|
|
1059
|
+
this.formItem = record
|
|
1060
|
+
this.formSchema = { type: 'object', properties }
|
|
1061
|
+
},
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Save an endpoint form: send the declared fields to `endpoint.url`
|
|
1065
|
+
* with `endpoint.method` (`PATCH` by default, or `PUT`), then toast and
|
|
1066
|
+
* refresh the page as an object save does.
|
|
1067
|
+
*
|
|
1068
|
+
* @param {object} entry The open-form action.
|
|
1069
|
+
* @param {object} formData The form's values.
|
|
1070
|
+
* @return {Promise<void>}
|
|
1071
|
+
* @spec openspec/changes/open-form-endpoint/specs/open-form-endpoint/spec.md#requirement-an-open-form-action-can-edit-a-record-through-an-endpoint
|
|
1072
|
+
*/
|
|
1073
|
+
async saveEndpointForm(entry, formData) {
|
|
1074
|
+
const dialog = this.$refs.formDialog
|
|
1075
|
+
const keys = Object.keys(this.formSchema?.properties || {})
|
|
1076
|
+
const body = Object.fromEntries(keys.filter((key) => key in formData).map((key) => [key, formData[key]]))
|
|
1077
|
+
const verb = String(entry.endpoint.method || 'PATCH').toUpperCase()
|
|
1078
|
+
const method = verb === 'PUT' ? 'put' : (verb === 'POST' ? 'post' : 'patch')
|
|
1079
|
+
try {
|
|
1080
|
+
const [{ default: axios }, { generateUrl }] = await Promise.all([
|
|
1081
|
+
import('@nextcloud/axios'),
|
|
1082
|
+
import('@nextcloud/router'),
|
|
1083
|
+
])
|
|
1084
|
+
const url = interpolateActionTarget(entry.endpoint.url, this.tokenCtx)
|
|
1085
|
+
const response = await axios[method](/^https?:\/\//i.test(url) ? url : generateUrl(url), body)
|
|
1086
|
+
if (dialog && typeof dialog.setResult === 'function') {
|
|
1087
|
+
dialog.setResult({ success: true })
|
|
1088
|
+
}
|
|
1089
|
+
const { showSuccess } = await import('@nextcloud/dialogs')
|
|
1090
|
+
if (typeof showSuccess === 'function') {
|
|
1091
|
+
showSuccess(this.tr(entry.successMessage) || t('nextcloud-vue', 'Saved.'))
|
|
1092
|
+
}
|
|
1093
|
+
const { emit } = await import('@nextcloud/event-bus')
|
|
1094
|
+
emit('cn:page:refresh', {})
|
|
1095
|
+
this.$emit('created', response && response.data)
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
const serverMessage = error && error.response && error.response.data
|
|
1098
|
+
&& (error.response.data.error || error.response.data.message)
|
|
1099
|
+
if (dialog && typeof dialog.setResult === 'function') {
|
|
1100
|
+
dialog.setResult({ error: serverMessage || this.tr(entry.errorMessage) || t('nextcloud-vue', 'Save failed.') })
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
|
|
1018
1105
|
/**
|
|
1019
1106
|
* Open (or skip) the `run-node` config dialog: GET the target node's
|
|
1020
1107
|
* describe endpoint (`or-flow-run-node`'s `GET .../{flowId}/{nodeId}/run`),
|
|
@@ -1125,6 +1212,10 @@ export default {
|
|
|
1125
1212
|
*/
|
|
1126
1213
|
async onFormConfirm(formData, dynamic = null) {
|
|
1127
1214
|
const entry = this.formEntry
|
|
1215
|
+
if (entry && entry.endpoint) {
|
|
1216
|
+
await this.saveEndpointForm(entry, formData)
|
|
1217
|
+
return
|
|
1218
|
+
}
|
|
1128
1219
|
const dialog = this.$refs.formDialog
|
|
1129
1220
|
const register = this.formRegister
|
|
1130
1221
|
const schema = (entry && entry.schema) || ''
|
|
@@ -1267,6 +1358,7 @@ export default {
|
|
|
1267
1358
|
closeForm() {
|
|
1268
1359
|
this.formEntry = null
|
|
1269
1360
|
this.formSchema = null
|
|
1361
|
+
this.formItem = null
|
|
1270
1362
|
},
|
|
1271
1363
|
},
|
|
1272
1364
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div class="cn-checklist-widget" data-testid="cn-checklist-widget">
|
|
8
|
+
<p v-if="items.length === 0" class="cn-checklist-widget__muted" data-testid="cn-checklist-empty">
|
|
9
|
+
{{ emptyLabel }}
|
|
10
|
+
</p>
|
|
11
|
+
<template v-else>
|
|
12
|
+
<p class="cn-checklist-widget__summary" data-testid="cn-checklist-summary">
|
|
13
|
+
{{ summary }}
|
|
14
|
+
</p>
|
|
15
|
+
<ul class="cn-checklist-widget__items">
|
|
16
|
+
<li v-for="item in items" :key="item.id" class="cn-checklist-widget__item">
|
|
17
|
+
<NcCheckboxRadioSwitch
|
|
18
|
+
:modelValue="item.checked"
|
|
19
|
+
:disabled="!writable || busy"
|
|
20
|
+
:data-testid="`cn-checklist-item-${item.id}`"
|
|
21
|
+
@update:modelValue="toggle(item, $event)">
|
|
22
|
+
{{ item.label }}
|
|
23
|
+
</NcCheckboxRadioSwitch>
|
|
24
|
+
<p v-if="item.description" class="cn-checklist-widget__description">
|
|
25
|
+
{{ item.description }}
|
|
26
|
+
</p>
|
|
27
|
+
</li>
|
|
28
|
+
</ul>
|
|
29
|
+
</template>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script>
|
|
34
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
35
|
+
import { NcCheckboxRadioSwitch } from '@nextcloud/vue'
|
|
36
|
+
import { readPath } from '../../utils/readPath.js'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* CnChecklistWidget — the checklist a record carries, ticked in place.
|
|
40
|
+
*
|
|
41
|
+
* The items are read off the record in view (`content.path`, `checklist` by
|
|
42
|
+
* default), each `{ id, label, description, checked }` unless `content`
|
|
43
|
+
* names other fields. A tick is written through the detail page's record
|
|
44
|
+
* source (`cnRecordChecklist`, provided by CnDetailPage when its source can
|
|
45
|
+
* check items, as the `tasks` source does through the task engine). The box
|
|
46
|
+
* follows the record the write answers with, so a refused tick leaves the
|
|
47
|
+
* item as it was. Without a source that can write, or on a record the source
|
|
48
|
+
* says can no longer be ticked, the list is read-only.
|
|
49
|
+
*
|
|
50
|
+
* An item without an id is left out: the write addresses an item by its id,
|
|
51
|
+
* and a box that cannot be saved would look saved.
|
|
52
|
+
*/
|
|
53
|
+
export default {
|
|
54
|
+
name: 'CnChecklistWidget',
|
|
55
|
+
|
|
56
|
+
components: { NcCheckboxRadioSwitch },
|
|
57
|
+
|
|
58
|
+
inject: {
|
|
59
|
+
cnTranslate: { default: () => (key) => key },
|
|
60
|
+
cnObjectContext: { default: null },
|
|
61
|
+
cnRecordChecklist: { default: null },
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
props: {
|
|
65
|
+
/**
|
|
66
|
+
* The widget configuration: `path` (where the items sit on the record,
|
|
67
|
+
* default `checklist`), `idField` / `labelField` / `descriptionField` /
|
|
68
|
+
* `checkedField` (default `id`, `label`, `description`, `checked`),
|
|
69
|
+
* `emptyText` (shown when the record carries no items).
|
|
70
|
+
*/
|
|
71
|
+
content: {
|
|
72
|
+
type: Object,
|
|
73
|
+
default: () => ({}),
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/** The record in view, as the detail page hands it to a widget. */
|
|
77
|
+
objectData: {
|
|
78
|
+
type: Object,
|
|
79
|
+
default: null,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
data() {
|
|
84
|
+
return {
|
|
85
|
+
/** Whether a tick is being written, so two cannot cross. */
|
|
86
|
+
busy: false,
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
computed: {
|
|
91
|
+
/**
|
|
92
|
+
* The record: the prop, else the page's injected context.
|
|
93
|
+
*
|
|
94
|
+
* @return {object|null}
|
|
95
|
+
*/
|
|
96
|
+
record() {
|
|
97
|
+
if (this.objectData) {
|
|
98
|
+
return this.objectData
|
|
99
|
+
}
|
|
100
|
+
const context = this.cnObjectContext
|
|
101
|
+
const value = context && typeof context === 'object' && 'value' in context ? context.value : context
|
|
102
|
+
return value?.object || null
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The items, normalised, without the ones that have no id.
|
|
107
|
+
*
|
|
108
|
+
* @return {Array<{id: string, label: string, description: string, checked: boolean}>}
|
|
109
|
+
*/
|
|
110
|
+
items() {
|
|
111
|
+
const content = this.content || {}
|
|
112
|
+
const list = this.record ? readPath(this.record, content.path || 'checklist') : null
|
|
113
|
+
if (!Array.isArray(list)) {
|
|
114
|
+
return []
|
|
115
|
+
}
|
|
116
|
+
return list
|
|
117
|
+
.filter((item) => item && typeof item === 'object' && String(item[content.idField || 'id'] ?? '').trim() !== '')
|
|
118
|
+
.map((item) => {
|
|
119
|
+
const id = String(item[content.idField || 'id']).trim()
|
|
120
|
+
return {
|
|
121
|
+
id,
|
|
122
|
+
label: String(item[content.labelField || 'label'] ?? '').trim() || id,
|
|
123
|
+
description: String(item[content.descriptionField || 'description'] ?? '').trim(),
|
|
124
|
+
checked: item[content.checkedField || 'checked'] === true,
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* "2 of 4 done".
|
|
131
|
+
*
|
|
132
|
+
* @return {string}
|
|
133
|
+
*/
|
|
134
|
+
summary() {
|
|
135
|
+
const done = this.items.filter((item) => item.checked).length
|
|
136
|
+
return t('nextcloud-vue', '{done} of {total} done', { done, total: this.items.length })
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Whether a tick can be written for this record.
|
|
141
|
+
*
|
|
142
|
+
* @return {boolean}
|
|
143
|
+
*/
|
|
144
|
+
writable() {
|
|
145
|
+
return Boolean(this.cnRecordChecklist && this.cnRecordChecklist.canCheck === true)
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/** @return {string} The empty text, through the app's catalogue. */
|
|
149
|
+
emptyLabel() {
|
|
150
|
+
const text = this.content?.emptyText
|
|
151
|
+
return typeof text === 'string' && text !== '' ? this.cnTranslate(text) : t('nextcloud-vue', 'Nothing to check off')
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
methods: {
|
|
156
|
+
/**
|
|
157
|
+
* Write one tick through the page's record source.
|
|
158
|
+
*
|
|
159
|
+
* @param {{id: string}} item The item.
|
|
160
|
+
* @param {boolean} checked The new state.
|
|
161
|
+
* @return {Promise<void>}
|
|
162
|
+
*/
|
|
163
|
+
async toggle(item, checked) {
|
|
164
|
+
if (!this.writable || this.busy) {
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
this.busy = true
|
|
168
|
+
try {
|
|
169
|
+
await this.cnRecordChecklist.check(item.id, checked === true)
|
|
170
|
+
} finally {
|
|
171
|
+
this.busy = false
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
</script>
|
|
177
|
+
|
|
178
|
+
<style scoped>
|
|
179
|
+
.cn-checklist-widget {
|
|
180
|
+
display: flex;
|
|
181
|
+
flex-direction: column;
|
|
182
|
+
gap: 8px;
|
|
183
|
+
min-width: 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.cn-checklist-widget__summary,
|
|
187
|
+
.cn-checklist-widget__muted {
|
|
188
|
+
margin: 0;
|
|
189
|
+
color: var(--color-text-maxcontrast);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.cn-checklist-widget__items {
|
|
193
|
+
margin: 0;
|
|
194
|
+
padding: 0;
|
|
195
|
+
list-style: none;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.cn-checklist-widget__item {
|
|
199
|
+
min-width: 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.cn-checklist-widget__description {
|
|
203
|
+
margin: 0 0 4px 36px;
|
|
204
|
+
color: var(--color-text-maxcontrast);
|
|
205
|
+
}
|
|
206
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnChecklistWidget — self-registers the `checklist` widget type (the
|
|
6
|
+
* checklist a record carries, ticked in place through the page's record
|
|
7
|
+
* source) into the shared dashboardWidgetRegistry at module load. Renderer
|
|
8
|
+
* only: the items come from the record, so the Add-widget picker does not
|
|
9
|
+
* offer it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import CnChecklistWidget from './CnChecklistWidget.vue'
|
|
13
|
+
import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
|
|
14
|
+
|
|
15
|
+
registerDashboardWidget('checklist', {
|
|
16
|
+
renderer: CnChecklistWidget,
|
|
17
|
+
form: null,
|
|
18
|
+
defaultContent: {
|
|
19
|
+
path: 'checklist',
|
|
20
|
+
},
|
|
21
|
+
displayName: 'Checklist',
|
|
22
|
+
icon: 'CheckboxMarkedOutline',
|
|
23
|
+
surfaces: ['detail-page'],
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export { CnChecklistWidget }
|
|
27
|
+
export default CnChecklistWidget
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<CnHeaderTrail
|
|
38
38
|
:breadcrumb="breadcrumb"
|
|
39
39
|
:current="title ? resolvedTitle : ''"
|
|
40
|
-
:meta="
|
|
40
|
+
:meta="resolvedHeaderMeta" />
|
|
41
41
|
<!-- `dateRange.placement: "description"`: the segmented period
|
|
42
42
|
group ends the description line ("Week 41 · alle teams ·
|
|
43
43
|
[Deze week | Dit kwartaal | Dit jaar]"). -->
|
|
@@ -2178,6 +2178,31 @@ export default {
|
|
|
2178
2178
|
* @spec openspec/changes/dashboard-greeting-label/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-lead-with-a-label-or-the-date
|
|
2179
2179
|
* @return {string}
|
|
2180
2180
|
*/
|
|
2181
|
+
/**
|
|
2182
|
+
* `headerMeta` through the host translate, with `{today}` written out as
|
|
2183
|
+
* today's date (DqDagAfsluiten "Dag afsluiten, woensdag 7 oktober").
|
|
2184
|
+
*
|
|
2185
|
+
* @spec openspec/changes/record-list-notes/specs/record-list-notes/spec.md#requirement-a-dashboard-header-meta-can-name-today
|
|
2186
|
+
* @return {string}
|
|
2187
|
+
*/
|
|
2188
|
+
resolvedHeaderMeta() {
|
|
2189
|
+
if (!this.headerMeta) {
|
|
2190
|
+
return ''
|
|
2191
|
+
}
|
|
2192
|
+
const meta = this.effectiveTranslate(this.headerMeta)
|
|
2193
|
+
if (!meta.includes('{today}')) {
|
|
2194
|
+
return meta
|
|
2195
|
+
}
|
|
2196
|
+
const options = { weekday: 'long', day: 'numeric', month: 'long' }
|
|
2197
|
+
let today
|
|
2198
|
+
try {
|
|
2199
|
+
today = new Intl.DateTimeFormat(getCanonicalLocale() || undefined, options).format(new Date())
|
|
2200
|
+
} catch {
|
|
2201
|
+
today = new Intl.DateTimeFormat(undefined, options).format(new Date())
|
|
2202
|
+
}
|
|
2203
|
+
return meta.split('{today}').join(today)
|
|
2204
|
+
},
|
|
2205
|
+
|
|
2181
2206
|
subtitleText() {
|
|
2182
2207
|
const description = this.description ? this.resolvedDescription : ''
|
|
2183
2208
|
const dateOnly = this.greeting === 'date'
|
|
@@ -428,6 +428,8 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
|
|
|
428
428
|
| `sideColumn` | Array | `[]` | Widget definitions or widget ids, rendered as a column of cards beside the body. An entry with `tabs` shows only while one of those body tabs is open. |
|
|
429
429
|
| `sideColumnFrom` | String | `''` | A field path, read like a `visibleWhen` field (e.g. `@context.caseType.sideCards`), whose list of ids picks the `sideColumn` entries this record shows and their order. Any other value keeps `sideColumn` as declared. |
|
|
430
430
|
| `tabsFrom` | String | `''` | A field path, read like a `visibleWhen` field, whose list of tab ids picks the tabs of the page's `tabs` widgets and their order for this record: `@context.caseType.tabs` reads the list a case type keeps (DqAanbesteding: Inschrijvingen, Publicaties, Bestanden, Historie). A tab's id is its `id`, else its `widgetId`, or the string entry. An empty or missing list keeps the declared tabs; edit mode shows them all. |
|
|
431
|
+
| `verbEndpoints` | Object | `null` | App routes that replace a record source's own route for a verb, by verb id (manifest `config.verbEndpoints`), such as `{ "complete": "/apps/dossiq/api/case-tasks/{id}/complete" }` for an app that checks something of its own before the engine completes the task. `{id}` is the record id. Only read with `entitySource`. |
|
|
432
|
+
| `accessPurpose` | Object | `null` | Ask for a purpose before the record opens (manifest `config.accessPurpose`): `{ when, options, url, title?, description? }`. `when` is a `visibleWhen` condition on the record and its `visibilityContext` records; `options` are the purposes offered (`{ value, label }`; "Other" with a written purpose is always added); `url` is the app endpoint that records the purpose (POST `{ purpose, note }`, `{objectId}` filled in). Nothing of the record shows until the endpoint accepts, and the same record is not asked again in the same browser session. A refusal keeps the record closed. `null` asks nothing. |
|
|
431
433
|
| `isAdmin` | Boolean \| null | `null` | Shows `adminOnly` header actions. `null` reads it from Nextcloud. |
|
|
432
434
|
|
|
433
435
|
## The record as a place
|