@conduction/nextcloud-vue 2.114.0 → 2.115.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/CnAdvancedFormDialog/CnPropertyValueCell.vue.js +1 -1
- package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue2.js +1 -1
- package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdvancedFormDialog/CnPropertyValueCell.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +6 -2
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +2 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +19 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -3
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +2 -2
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +37 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/rowsToCsv.js +90 -0
- package/dist/esm/components/CnIndexPage/rowsToCsv.js.map +1 -0
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue.js +9 -0
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue.js.map +1 -0
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js +161 -0
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js +97 -0
- package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue.js +9 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue.js.map +1 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue2.js +63 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue2.js.map +1 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue3.js +54 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue3.js.map +1 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js +15 -9
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +55 -63
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +2 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +13 -4
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +2 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue.js +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue2.js.map +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue3.js.map +1 -1
- 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 +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue.js +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +2 -2
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +32 -20
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +43 -11
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useTaskInboxStore.js +41 -0
- package/dist/esm/composables/useTaskInboxStore.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +4 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +9 -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 +482 -330
- package/dist/esm/utils/contentHeaderLink.js +37 -1
- package/dist/esm/utils/contentHeaderLink.js.map +1 -1
- package/dist/esm/utils/declaredMacros.js +31 -0
- package/dist/esm/utils/declaredMacros.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +2673 -1977
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +482 -330
- package/l10n/en.json +4 -1
- package/l10n/nl.json +9 -1
- package/package.json +1 -1
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +1 -1
- package/src/components/CnCellRenderer/CnCellRenderer.vue +6 -2
- package/src/components/CnChoiceCards/CnChoiceCards.vue +2 -1
- package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -3
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
- package/src/components/CnIndexPage/CnIndexPage.vue +37 -1
- package/src/components/CnIndexPage/rowsToCsv.js +87 -0
- package/src/components/CnMacrosWidget/CnMacrosWidget.md +30 -0
- package/src/components/CnMacrosWidget/CnMacrosWidget.vue +266 -0
- package/src/components/CnMacrosWidget/index.js +6 -0
- package/src/components/CnRetentionWidget/CnRetentionRecomputeDialog.vue +104 -0
- package/src/components/CnRetentionWidget/CnRetentionWidget.vue +53 -38
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +2 -1
- package/src/components/CnSetupWizard/CnSetupWizard.vue +28 -4
- package/src/components/CnStepper/CnStepper.vue +2 -1
- package/src/components/CnTabsWidget/CnTabsWidget.vue +11 -6
- package/src/components/CnTasksWidget/CnTasksWidget.vue +2 -2
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +12 -0
- package/src/components/index.js +1 -0
- package/src/composables/indexSources.js +43 -11
- package/src/composables/useTaskInboxStore.js +41 -0
- package/src/css/look-board-detail.css +4 -3
- package/src/css/look-board-index.css +62 -0
- package/src/css/look-board.css +20 -0
- package/src/index.js +1 -0
- package/src/utils/contentHeaderLink.js +35 -0
- package/src/utils/declaredMacros.js +28 -0
- package/src/utils/libraryWidgetKeys.js +1 -0
package/l10n/en.json
CHANGED
|
@@ -1929,7 +1929,10 @@
|
|
|
1929
1929
|
"or": "or",
|
|
1930
1930
|
"no value": "no value",
|
|
1931
1931
|
"yes": "yes",
|
|
1932
|
-
"no": "no"
|
|
1932
|
+
"no": "no",
|
|
1933
|
+
"Recompute nomination": "Recompute nomination",
|
|
1934
|
+
"Recompute": "Recompute",
|
|
1935
|
+
"Task deadline": "Deadline"
|
|
1933
1936
|
},
|
|
1934
1937
|
"plurals": {
|
|
1935
1938
|
"{count} member": [
|
package/l10n/nl.json
CHANGED
|
@@ -2141,7 +2141,15 @@
|
|
|
2141
2141
|
"or": "of",
|
|
2142
2142
|
"no value": "geen waarde",
|
|
2143
2143
|
"yes": "ja",
|
|
2144
|
-
"no": "nee"
|
|
2144
|
+
"no": "nee",
|
|
2145
|
+
"Recompute nomination": "Nominatie herberekenen",
|
|
2146
|
+
"Recompute": "Herberekenen",
|
|
2147
|
+
"Task deadline": "Deadline",
|
|
2148
|
+
"The macro did not run: {error}": "De macro is niet uitgevoerd: {error}",
|
|
2149
|
+
"Last run: {name}, {date}": "Laatst uitgevoerd: {name}, {date}",
|
|
2150
|
+
"This record has no macros": "Er zijn geen macro's voor dit item",
|
|
2151
|
+
"Running …": "Bezig …",
|
|
2152
|
+
"Execute": "Uitvoeren"
|
|
2145
2153
|
},
|
|
2146
2154
|
"plurals": {
|
|
2147
2155
|
"{count} member": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.115.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": {
|
|
@@ -251,7 +251,7 @@ export default {
|
|
|
251
251
|
/** Full JSON schema object */
|
|
252
252
|
schema: { type: Object, default: null },
|
|
253
253
|
/** Resolved current value (formData[key] ?? objectValue) */
|
|
254
|
-
value: { type: [
|
|
254
|
+
value: { type: [String, Number, Boolean, Object, Array], default: null }, // eslint-disable-line vue/prefer-prop-type-boolean-first -- an empty field value must stay a string
|
|
255
255
|
/** Whether this property is editable at all */
|
|
256
256
|
isEditable: { type: Boolean, default: true },
|
|
257
257
|
/** Whether this row is currently selected for editing */
|
|
@@ -456,9 +456,13 @@ export default {
|
|
|
456
456
|
},
|
|
457
457
|
|
|
458
458
|
props: {
|
|
459
|
-
/**
|
|
459
|
+
/**
|
|
460
|
+
* The raw cell value. String precedes Boolean: with Boolean first Vue
|
|
461
|
+
* casts an empty string to `true`, and an empty cell drew a check mark.
|
|
462
|
+
*/
|
|
460
463
|
value: {
|
|
461
|
-
type
|
|
464
|
+
// eslint-disable-next-line vue/prefer-prop-type-boolean-first -- an empty string must stay a string
|
|
465
|
+
type: [String, Number, Boolean, Array, Object],
|
|
462
466
|
default: null,
|
|
463
467
|
},
|
|
464
468
|
|
|
@@ -180,7 +180,8 @@ export default {
|
|
|
180
180
|
* @type {string|number|boolean|Array|null}
|
|
181
181
|
*/
|
|
182
182
|
modelValue: {
|
|
183
|
-
type
|
|
183
|
+
// eslint-disable-next-line vue/prefer-prop-type-boolean-first -- an option value of '' must stay a string
|
|
184
|
+
type: [String, Number, Boolean, Array, Object],
|
|
184
185
|
default: null,
|
|
185
186
|
},
|
|
186
187
|
|
|
@@ -42,11 +42,28 @@
|
|
|
42
42
|
group ends the description line ("Week 41 · alle teams ·
|
|
43
43
|
[Deze week | Dit kwartaal | Dit jaar]"). -->
|
|
44
44
|
<div
|
|
45
|
-
v-if="datePeriodInDescription || descriptionPageFilters.length > 0"
|
|
45
|
+
v-if="datePeriodInDescription || descriptionPageFilters.length > 0 || descriptionSelectFilters.length > 0"
|
|
46
46
|
class="cn-dashboard-page__description cn-dashboard-page__description--with-period"
|
|
47
47
|
data-testid="cn-dashboard-page-subtitle">
|
|
48
48
|
<span v-if="subtitleText">{{ subtitleText }}</span>
|
|
49
49
|
<span v-if="subtitleText" aria-hidden="true">·</span>
|
|
50
|
+
<!-- A select page filter with `placement: "description"`
|
|
51
|
+
(DqDoorlooptijd "alle zaaktypen"): a compact select on the
|
|
52
|
+
description line, before the period group. -->
|
|
53
|
+
<template v-for="pf in descriptionSelectFilters" :key="pf.key">
|
|
54
|
+
<NcSelect
|
|
55
|
+
class="cn-dashboard-page__description-select"
|
|
56
|
+
:modelValue="selectedPageFilterOption(pf)"
|
|
57
|
+
:options="pageFilterSegments(pf)"
|
|
58
|
+
:clearable="pf.clearable === true"
|
|
59
|
+
:placeholder="pf.placeholder ? effectiveTranslate(pf.placeholder) : undefined"
|
|
60
|
+
:loading="pageFilterLoading[pf.key] === true"
|
|
61
|
+
:ariaLabelCombobox="pf.label ? effectiveTranslate(pf.label) : pf.key"
|
|
62
|
+
label="label"
|
|
63
|
+
:data-testid="'cn-page-filter-' + pf.key"
|
|
64
|
+
@update:modelValue="onPageFilterChange(pf, $event)" />
|
|
65
|
+
<span aria-hidden="true">·</span>
|
|
66
|
+
</template>
|
|
50
67
|
<CnSegmentedControl
|
|
51
68
|
v-if="datePeriodInDescription"
|
|
52
69
|
size="compact"
|
|
@@ -1721,7 +1738,8 @@ export default {
|
|
|
1721
1738
|
* mount. The control is a select, or with `control: 'segmented'` a
|
|
1722
1739
|
* compact segmented group; `placement: 'description'` puts a segmented
|
|
1723
1740
|
* group on the header's description line, after the period group
|
|
1724
|
-
* (DqProcesanalyse "Gemiddeld | Mediaan")
|
|
1741
|
+
* (DqProcesanalyse "Gemiddeld | Mediaan"), and a select before it
|
|
1742
|
+
* (DqDoorlooptijd "alle zaaktypen"). An endpoint widget can read
|
|
1725
1743
|
* the value in its `responsePath` too (`phaseDays.@workspace.measure`).
|
|
1726
1744
|
* `optionsFrom` reads the options instead of listing them: `{ url,
|
|
1727
1745
|
* params?, responsePath?, valueField?, labelField? }` from an app
|
|
@@ -2446,10 +2464,26 @@ export default {
|
|
|
2446
2464
|
* @return {Array<object>}
|
|
2447
2465
|
*/
|
|
2448
2466
|
rowPageFilters() {
|
|
2449
|
-
const inDescription = this.descriptionPageFilters
|
|
2467
|
+
const inDescription = [...this.descriptionPageFilters, ...this.descriptionSelectFilters]
|
|
2450
2468
|
return (this.pageFilters || []).filter((pf) => pf && pf.key && !inDescription.includes(pf))
|
|
2451
2469
|
},
|
|
2452
2470
|
|
|
2471
|
+
/**
|
|
2472
|
+
* The select page filters drawn on the description line
|
|
2473
|
+
* (`placement: "description"`, any control but `segmented`), before
|
|
2474
|
+
* the period group: DqDoorlooptijd's "alle zaaktypen". Without a
|
|
2475
|
+
* header row they fall back to the filter row.
|
|
2476
|
+
*
|
|
2477
|
+
* @spec openspec/changes/dashboard-select-filter-in-description/specs/dashboard-select-filter-in-description/spec.md#requirement-a-select-page-filter-can-sit-on-the-description-line
|
|
2478
|
+
* @return {Array<object>}
|
|
2479
|
+
*/
|
|
2480
|
+
descriptionSelectFilters() {
|
|
2481
|
+
if (!this.showHeader) {
|
|
2482
|
+
return []
|
|
2483
|
+
}
|
|
2484
|
+
return (this.pageFilters || []).filter((pf) => pf && pf.key && pf.control !== 'segmented' && pf.placement === 'description')
|
|
2485
|
+
},
|
|
2486
|
+
|
|
2453
2487
|
/**
|
|
2454
2488
|
* The edit button's label through the app's translate: a manifest's
|
|
2455
2489
|
* `editLabel` ("Edit layout", DqDashboard) reads in the user's language.
|
|
@@ -5186,6 +5220,14 @@ export default {
|
|
|
5186
5220
|
gap: 8px;
|
|
5187
5221
|
}
|
|
5188
5222
|
|
|
5223
|
+
/* A select on the description line takes the width of its choice, not a
|
|
5224
|
+
form field's, and no margin of its own. */
|
|
5225
|
+
.cn-dashboard-page__description-select {
|
|
5226
|
+
min-width: 180px;
|
|
5227
|
+
max-width: 280px;
|
|
5228
|
+
margin: 0;
|
|
5229
|
+
}
|
|
5230
|
+
|
|
5189
5231
|
.cn-dashboard-page__date-segmented {
|
|
5190
5232
|
display: flex;
|
|
5191
5233
|
align-items: center;
|
|
@@ -333,7 +333,7 @@ import CnDetailWidgetHeaderItems from './CnDetailWidgetHeaderItems.vue'
|
|
|
333
333
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
334
334
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
335
335
|
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
336
|
-
import { widgetHeaderLink } from '../../utils/contentHeaderLink.js'
|
|
336
|
+
import { resolveHeaderLink, widgetHeaderLink } from '../../utils/contentHeaderLink.js'
|
|
337
337
|
import { pickDeclaredProps } from '../../utils/declaredProps.js'
|
|
338
338
|
import { unwrapAppConfig } from '../../utils/formatMetric.js'
|
|
339
339
|
import { isActivityWidget } from '../../utils/headerMeta.js'
|
|
@@ -712,7 +712,7 @@ export default {
|
|
|
712
712
|
* @return {object|null} `{ label, route?, params?, query?, href? }` or null.
|
|
713
713
|
*/
|
|
714
714
|
cardHeaderLink() {
|
|
715
|
-
return this.rendererHeader.link || (this.showHeaderLink ? widgetHeaderLink(this.widget) : null)
|
|
715
|
+
return this.rendererHeader.link || (this.showHeaderLink ? resolveHeaderLink(widgetHeaderLink(this.widget), { objectId: this.objectId, object: this.object }) : null)
|
|
716
716
|
},
|
|
717
717
|
|
|
718
718
|
/**
|
|
@@ -1431,6 +1431,7 @@ import { buildDefaultActions } from './defaultActions.js'
|
|
|
1431
1431
|
import { dispatchAction } from './manifestActionDispatch.js'
|
|
1432
1432
|
import { applyManualOrder, dropInOrder, manualOrderKey, moveInOrder, visibleIdsOf } from './manualOrder.js'
|
|
1433
1433
|
import { orderColumns, personalColumnsKey, reconcilePersonalColumns } from './personalColumns.js'
|
|
1434
|
+
import { rowsToCsv } from './rowsToCsv.js'
|
|
1434
1435
|
import { createSelfModeActions, resolveRowTarget } from './selfModeActions.js'
|
|
1435
1436
|
import { applyRowPatches, normalisePaneWidth, rowIdOf, splitLayoutFor } from './splitView.js'
|
|
1436
1437
|
import { useNamedSource } from './useNamedSource.js'
|
|
@@ -9510,6 +9511,8 @@ export default {
|
|
|
9510
9511
|
this.onHeaderAction({ action: button.run, id: button.run })
|
|
9511
9512
|
} else if (this.register && this.exportSchemaSlug) {
|
|
9512
9513
|
this.onExportClick(button.format)
|
|
9514
|
+
} else if (this.isNamedSource && !this.hasMassExportListener()) {
|
|
9515
|
+
this.downloadNamedRows()
|
|
9513
9516
|
} else {
|
|
9514
9517
|
this.showExportDialog = true
|
|
9515
9518
|
}
|
|
@@ -9536,13 +9539,46 @@ export default {
|
|
|
9536
9539
|
* @return {boolean}
|
|
9537
9540
|
*/
|
|
9538
9541
|
headerExportAvailable(button) {
|
|
9539
|
-
if (!this.isNamedSource || (button && button.run)) {
|
|
9542
|
+
if (!this.isNamedSource || (button && button.run) || this.hasMassExportListener()) {
|
|
9540
9543
|
return true
|
|
9541
9544
|
}
|
|
9545
|
+
// A named source exports the rows it holds as CSV (downloadNamedRows).
|
|
9546
|
+
return this.renderedColumns.some((col) => col && typeof col.key === 'string' && col.key !== '')
|
|
9547
|
+
},
|
|
9548
|
+
|
|
9549
|
+
/**
|
|
9550
|
+
* Whether the consumer listens to `@mass-export`.
|
|
9551
|
+
*
|
|
9552
|
+
* @return {boolean}
|
|
9553
|
+
*/
|
|
9554
|
+
hasMassExportListener() {
|
|
9542
9555
|
const props = this.$ && this.$.vnode && this.$.vnode.props
|
|
9543
9556
|
return Boolean(props && typeof props.onMassExport === 'function')
|
|
9544
9557
|
},
|
|
9545
9558
|
|
|
9559
|
+
/**
|
|
9560
|
+
* Download the rows of a named or endpoint source as CSV: every row the
|
|
9561
|
+
* source holds for the current query, with the table's columns. Such a
|
|
9562
|
+
* source has no server export, and a Download that does nothing is
|
|
9563
|
+
* worse than none.
|
|
9564
|
+
*
|
|
9565
|
+
* @spec openspec/changes/index-named-source-export/specs/index-named-source-export/spec.md#requirement-a-named-source-list-downloads-its-rows
|
|
9566
|
+
* @return {void}
|
|
9567
|
+
*/
|
|
9568
|
+
downloadNamedRows() {
|
|
9569
|
+
const csv = rowsToCsv(this.namedRows, this.renderedColumns)
|
|
9570
|
+
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8' })
|
|
9571
|
+
const url = URL.createObjectURL(blob)
|
|
9572
|
+
const link = document.createElement('a')
|
|
9573
|
+
const name = String(this.title || this.entitySource || 'export').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'export'
|
|
9574
|
+
link.href = url
|
|
9575
|
+
link.download = `${name}.csv`
|
|
9576
|
+
document.body.appendChild(link)
|
|
9577
|
+
link.click()
|
|
9578
|
+
link.remove()
|
|
9579
|
+
URL.revokeObjectURL(url)
|
|
9580
|
+
},
|
|
9581
|
+
|
|
9546
9582
|
/**
|
|
9547
9583
|
* Click dispatch from CnActionsBar's `@header-action`. Looks
|
|
9548
9584
|
* up the resolved entry by id, invokes its handler if any,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* The rows a list shows, as a CSV file: one column per table column, headed by
|
|
6
|
+
* its label. Used by an index page whose rows come from a named or endpoint
|
|
7
|
+
* source, which has no server export of its own.
|
|
8
|
+
*
|
|
9
|
+
* @spec openspec/changes/index-named-source-export/specs/index-named-source-export/spec.md#requirement-a-named-source-list-downloads-its-rows
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A value as the text of one CSV cell. A list joins its items, a record reads
|
|
14
|
+
* its name, an empty value is an empty cell.
|
|
15
|
+
*
|
|
16
|
+
* @param {unknown} value The cell value.
|
|
17
|
+
* @return {string} The cell text.
|
|
18
|
+
*/
|
|
19
|
+
export function cellText(value) {
|
|
20
|
+
if (value === null || value === undefined) {
|
|
21
|
+
return ''
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
return value.map(cellText).filter((item) => item !== '').join('; ')
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === 'object') {
|
|
27
|
+
for (const key of ['title', 'name', 'label', 'displayName', 'id']) {
|
|
28
|
+
if (value[key] !== undefined && value[key] !== null && typeof value[key] !== 'object') {
|
|
29
|
+
return String(value[key])
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return JSON.stringify(value)
|
|
33
|
+
}
|
|
34
|
+
return String(value)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One CSV field: quoted when it holds a separator, a quote or a line break,
|
|
39
|
+
* and a leading `=`, `+`, `-` or `@` is neutralised with a `'` so a
|
|
40
|
+
* spreadsheet does not read the cell as a formula.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} text The cell text.
|
|
43
|
+
* @return {string} The field.
|
|
44
|
+
*/
|
|
45
|
+
export function csvField(text) {
|
|
46
|
+
let out = String(text)
|
|
47
|
+
if (/^[=+\-@\t\r]/.test(out)) {
|
|
48
|
+
out = `'${out}`
|
|
49
|
+
}
|
|
50
|
+
return /[",\r\n]/.test(out) ? `"${out.replace(/"/g, '""')}"` : out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The value of a column in a row: a dotted key reads a nested field, a flat
|
|
55
|
+
* key falls back to the row's `@self`, as the table reads it.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} row The row.
|
|
58
|
+
* @param {string} key The column key.
|
|
59
|
+
* @return {unknown} The value.
|
|
60
|
+
*/
|
|
61
|
+
function columnValue(row, key) {
|
|
62
|
+
if (key.includes('.')) {
|
|
63
|
+
return key.split('.').reduce((obj, part) => obj?.[part], row)
|
|
64
|
+
}
|
|
65
|
+
if (row?.[key] === undefined && row?.['@self'] && typeof row['@self'] === 'object') {
|
|
66
|
+
return row['@self'][key]
|
|
67
|
+
}
|
|
68
|
+
return row?.[key]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The rows as CSV text, a header line of the column labels first. Columns
|
|
73
|
+
* without a key (selection, row actions) are left out.
|
|
74
|
+
*
|
|
75
|
+
* @param {Array<object>} rows The rows.
|
|
76
|
+
* @param {Array<{key: string, label?: string}>} columns The table columns.
|
|
77
|
+
* @return {string} The CSV text, lines ended with CRLF.
|
|
78
|
+
*/
|
|
79
|
+
export function rowsToCsv(rows, columns) {
|
|
80
|
+
const cols = (Array.isArray(columns) ? columns : [])
|
|
81
|
+
.filter((col) => col && typeof col.key === 'string' && col.key !== '' && col.widget !== 'actions')
|
|
82
|
+
const lines = [cols.map((col) => csvField(col.label || col.key)).join(',')]
|
|
83
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
84
|
+
lines.push(cols.map((col) => csvField(cellText(columnValue(row, col.key)))).join(','))
|
|
85
|
+
}
|
|
86
|
+
return lines.join('\r\n') + '\r\n'
|
|
87
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# CnMacrosWidget
|
|
2
|
+
|
|
3
|
+
The macros a record's schema declares, each with a button that runs it: `type: "macros"` on a detail page.
|
|
4
|
+
|
|
5
|
+
A macro is an OpenRegister declared action (`configuration['x-openregister-action']`) with `macro: true` and a bound
|
|
6
|
+
manual flow. The widget lists them in declaration order and runs the pressed one through
|
|
7
|
+
`POST /api/objects/{register}/{schema}/{id}/actions/{action}`, as the person and under the action's own right. After a
|
|
8
|
+
run it asks the page to refresh (`cn:page:refresh`). The last run is read from the record's audit trail
|
|
9
|
+
(`action.macro`); when the trail cannot be read the line is left out.
|
|
10
|
+
|
|
11
|
+
## Content
|
|
12
|
+
|
|
13
|
+
| Key | Type | Description |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `description` | `string` | The line under the title, translated. |
|
|
16
|
+
| `emptyText` | `string` | Shown when the schema declares no macros, translated. |
|
|
17
|
+
|
|
18
|
+
## Example
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{ "id": "case-macros", "type": "macros", "title": "Macros", "content": { "description": "Several steps at once; then you see what changed" } }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Props
|
|
25
|
+
|
|
26
|
+
| Prop | Type | Default | Description |
|
|
27
|
+
|------|------|---------|-------------|
|
|
28
|
+
| `content` | Object | `{}` | The widget's content above: `description` and `emptyText`, both translated |
|
|
29
|
+
| `objectData` | Object | `null` | The bound record |
|
|
30
|
+
| `objectId` | String \| Number | `''` | The bound record's id |
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<!--
|
|
7
|
+
CnMacrosWidget — the macros a record's schema declares, each with a button
|
|
8
|
+
that runs it (`type: "macros"`, DqZaakAssistent "Macro's").
|
|
9
|
+
|
|
10
|
+
A macro is a declared action (`configuration['x-openregister-action']`) with
|
|
11
|
+
`macro: true` and a bound manual flow; OpenRegister runs it as the person,
|
|
12
|
+
under the action's own right, through
|
|
13
|
+
`POST /api/objects/{register}/{schema}/{id}/actions/{action}`. The widget
|
|
14
|
+
lists what the schema declares and runs what is pressed: it adds no rights and
|
|
15
|
+
no list of its own. The last run comes from the record's audit trail
|
|
16
|
+
(`action.macro`), and after a run the page is asked to refresh.
|
|
17
|
+
-->
|
|
18
|
+
<template>
|
|
19
|
+
<div class="cn-macros-widget" data-testid="cn-macros-widget">
|
|
20
|
+
<p v-if="content.description" class="cn-macros-widget__description">
|
|
21
|
+
{{ tr(content.description) }}
|
|
22
|
+
</p>
|
|
23
|
+
<p v-if="loading" class="cn-macros-widget__muted">
|
|
24
|
+
{{ t('nextcloud-vue', 'Loading …') }}
|
|
25
|
+
</p>
|
|
26
|
+
<p v-else-if="macros.length === 0" class="cn-macros-widget__muted" data-testid="cn-macros-widget-empty">
|
|
27
|
+
{{ content.emptyText ? tr(content.emptyText) : t('nextcloud-vue', 'This record has no macros') }}
|
|
28
|
+
</p>
|
|
29
|
+
<ul v-else class="cn-macros-widget__list">
|
|
30
|
+
<li
|
|
31
|
+
v-for="macro in macros"
|
|
32
|
+
:key="macro.key"
|
|
33
|
+
class="cn-macros-widget__row"
|
|
34
|
+
:data-testid="`cn-macros-widget-row-${macro.key}`">
|
|
35
|
+
<div class="cn-macros-widget__text">
|
|
36
|
+
<span class="cn-macros-widget__name">{{ macro.name }}</span>
|
|
37
|
+
<span v-if="macro.description" class="cn-macros-widget__hint">{{ macro.description }}</span>
|
|
38
|
+
</div>
|
|
39
|
+
<NcButton
|
|
40
|
+
:disabled="running !== ''"
|
|
41
|
+
:data-testid="`cn-macros-widget-run-${macro.key}`"
|
|
42
|
+
@click="run(macro)">
|
|
43
|
+
{{ running === macro.key ? t('nextcloud-vue', 'Running …') : t('nextcloud-vue', 'Execute') }}
|
|
44
|
+
</NcButton>
|
|
45
|
+
</li>
|
|
46
|
+
</ul>
|
|
47
|
+
<NcNoteCard
|
|
48
|
+
v-if="error"
|
|
49
|
+
type="error"
|
|
50
|
+
data-testid="cn-macros-widget-error">
|
|
51
|
+
{{ error }}
|
|
52
|
+
</NcNoteCard>
|
|
53
|
+
<NcNoteCard
|
|
54
|
+
v-else-if="lastRun"
|
|
55
|
+
type="success"
|
|
56
|
+
data-testid="cn-macros-widget-last-run">
|
|
57
|
+
{{ t('nextcloud-vue', 'Last run: {name}, {date}', { name: lastRun.name, date: lastRun.date }) }}
|
|
58
|
+
</NcNoteCard>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<script>
|
|
63
|
+
import axios from '@nextcloud/axios'
|
|
64
|
+
import { emit } from '@nextcloud/event-bus'
|
|
65
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
66
|
+
import { generateUrl } from '@nextcloud/router'
|
|
67
|
+
import { NcButton, NcNoteCard } from '@nextcloud/vue'
|
|
68
|
+
import { declaredMacros } from '../../utils/declaredMacros.js'
|
|
69
|
+
|
|
70
|
+
export default {
|
|
71
|
+
name: 'CnMacrosWidget',
|
|
72
|
+
|
|
73
|
+
components: {
|
|
74
|
+
NcButton,
|
|
75
|
+
NcNoteCard,
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
inject: {
|
|
79
|
+
cnTranslate: { default: null },
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
props: {
|
|
83
|
+
/**
|
|
84
|
+
* The widget's content: `description` (the line under the title),
|
|
85
|
+
* `emptyText`, both translated.
|
|
86
|
+
*/
|
|
87
|
+
content: {
|
|
88
|
+
type: Object,
|
|
89
|
+
default: () => ({}),
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/** The bound record. */
|
|
93
|
+
objectData: {
|
|
94
|
+
type: Object,
|
|
95
|
+
default: null,
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/** The bound record's id. */
|
|
99
|
+
objectId: {
|
|
100
|
+
type: [String, Number],
|
|
101
|
+
default: '',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
data() {
|
|
106
|
+
return {
|
|
107
|
+
macros: [],
|
|
108
|
+
loading: false,
|
|
109
|
+
running: '',
|
|
110
|
+
error: '',
|
|
111
|
+
lastRun: null,
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
computed: {
|
|
116
|
+
/** @return {{register: string, schema: string, id: string}} Where the record lives. */
|
|
117
|
+
target() {
|
|
118
|
+
const self = this.objectData?.['@self'] || {}
|
|
119
|
+
return {
|
|
120
|
+
register: String(self.register ?? ''),
|
|
121
|
+
schema: String(self.schema ?? ''),
|
|
122
|
+
id: String(this.objectId || self.id || this.objectData?.id || ''),
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
watch: {
|
|
128
|
+
'target.schema': {
|
|
129
|
+
immediate: true,
|
|
130
|
+
handler() {
|
|
131
|
+
this.load()
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
methods: {
|
|
137
|
+
t,
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Translate an app-supplied text through the host app when it offers it.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} text The text.
|
|
143
|
+
* @return {string} The translated text.
|
|
144
|
+
*/
|
|
145
|
+
tr(text) {
|
|
146
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Read the schema's macros and the record's last macro run.
|
|
151
|
+
*
|
|
152
|
+
* @return {Promise<void>}
|
|
153
|
+
*/
|
|
154
|
+
async load() {
|
|
155
|
+
const { register, schema, id } = this.target
|
|
156
|
+
if (schema === '' || id === '') {
|
|
157
|
+
this.macros = []
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
this.loading = true
|
|
161
|
+
try {
|
|
162
|
+
const res = await axios.get(generateUrl('/apps/openregister/api/schemas/{schema}', { schema }), register ? { params: { register } } : undefined)
|
|
163
|
+
this.macros = declaredMacros(res?.data || null)
|
|
164
|
+
} catch {
|
|
165
|
+
this.macros = []
|
|
166
|
+
} finally {
|
|
167
|
+
this.loading = false
|
|
168
|
+
}
|
|
169
|
+
if (this.macros.length > 0) {
|
|
170
|
+
await this.loadLastRun()
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The record's most recent macro run, from its audit trail. Left empty
|
|
176
|
+
* when the trail cannot be read: it is a convenience, not a fact the
|
|
177
|
+
* widget may invent.
|
|
178
|
+
*
|
|
179
|
+
* @return {Promise<void>}
|
|
180
|
+
*/
|
|
181
|
+
async loadLastRun() {
|
|
182
|
+
const { register, schema, id } = this.target
|
|
183
|
+
try {
|
|
184
|
+
const res = await axios.get(
|
|
185
|
+
generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}/audit-trails', { register, schema, id }),
|
|
186
|
+
{ params: { action: 'action.macro', _limit: 1, '_sort[created]': 'DESC' } },
|
|
187
|
+
)
|
|
188
|
+
const entry = (res?.data?.results || [])[0]
|
|
189
|
+
const key = entry?.changed?.declaredAction
|
|
190
|
+
const macro = this.macros.find((candidate) => candidate.key === key)
|
|
191
|
+
this.lastRun = macro && entry.created
|
|
192
|
+
? { name: macro.name, date: new Date(entry.created).toLocaleString() }
|
|
193
|
+
: null
|
|
194
|
+
} catch {
|
|
195
|
+
this.lastRun = null
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Run one macro as the person, then ask the page to refresh.
|
|
201
|
+
*
|
|
202
|
+
* @param {{key: string, name: string}} macro The macro.
|
|
203
|
+
* @return {Promise<void>}
|
|
204
|
+
*/
|
|
205
|
+
async run(macro) {
|
|
206
|
+
const { register, schema, id } = this.target
|
|
207
|
+
this.running = macro.key
|
|
208
|
+
this.error = ''
|
|
209
|
+
try {
|
|
210
|
+
await axios.post(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}/actions/{action}', { register, schema, id, action: macro.key }))
|
|
211
|
+
this.lastRun = { name: macro.name, date: new Date().toLocaleString() }
|
|
212
|
+
emit('cn:page:refresh', { source: 'cn-macros-widget' })
|
|
213
|
+
} catch (e) {
|
|
214
|
+
this.error = t('nextcloud-vue', 'The macro did not run: {error}', { error: e?.response?.data?.error || e?.message || '' })
|
|
215
|
+
} finally {
|
|
216
|
+
this.running = ''
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
</script>
|
|
222
|
+
|
|
223
|
+
<style scoped>
|
|
224
|
+
.cn-macros-widget {
|
|
225
|
+
display: flex;
|
|
226
|
+
flex-direction: column;
|
|
227
|
+
gap: 12px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.cn-macros-widget__description,
|
|
231
|
+
.cn-macros-widget__muted {
|
|
232
|
+
margin: 0;
|
|
233
|
+
color: var(--color-text-maxcontrast);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.cn-macros-widget__list {
|
|
237
|
+
margin: 0;
|
|
238
|
+
padding: 0;
|
|
239
|
+
list-style: none;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.cn-macros-widget__row {
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
justify-content: space-between;
|
|
246
|
+
gap: 12px;
|
|
247
|
+
padding-block: 10px;
|
|
248
|
+
border-top: 1px solid var(--color-border);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.cn-macros-widget__text {
|
|
252
|
+
display: flex;
|
|
253
|
+
flex-direction: column;
|
|
254
|
+
gap: 2px;
|
|
255
|
+
min-width: 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.cn-macros-widget__name {
|
|
259
|
+
font-weight: 600;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.cn-macros-widget__hint {
|
|
263
|
+
color: var(--color-text-maxcontrast);
|
|
264
|
+
font-size: 13px;
|
|
265
|
+
}
|
|
266
|
+
</style>
|