@conduction/nextcloud-vue 2.98.0 → 2.100.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/CnActionsMenu/CnActionsMenu.vue2.js +12 -0
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +11 -2
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +6 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +2 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +25 -8
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +7 -4
- 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 -8
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +13 -4
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +54 -3
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +16 -2
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue.js +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js +42 -7
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js +121 -57
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js +10 -0
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js +2 -1
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +136 -7
- 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/CnOrderedListField/CnOrderedListField.vue.js +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +26 -7
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +78 -17
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +68 -7
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +256 -240
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +9 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js.map +1 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +265 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +191 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js +9 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js.map +1 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js +537 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js.map +1 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js +99 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js.map +1 -0
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +18 -70
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +6 -44
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +16 -2
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +40 -25
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.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/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +7 -7
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js +9 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js.map +1 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js +472 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js +231 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnWorkingCalendarWidget/index.js +27 -0
- package/dist/esm/components/CnWorkingCalendarWidget/index.js.map +1 -0
- package/dist/esm/composables/useFlowStore.js +2 -1
- package/dist/esm/composables/useFlowStore.js.map +1 -1
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +101 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +115 -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 +999 -233
- 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/workingCalendarMonth.js +85 -0
- package/dist/esm/utils/workingCalendarMonth.js.map +1 -0
- package/dist/nextcloud-vue.cjs.js +10314 -7778
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +999 -233
- package/l10n/en.json +101 -1
- package/l10n/nl.json +115 -1
- package/package.json +1 -1
- package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +7 -1
- package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +29 -9
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +13 -4
- package/src/components/CnFactsWidget/CnFactsWidget.vue +65 -3
- package/src/components/CnFlowsPage/CnFlowBoard.vue +77 -2
- package/src/components/CnFlowsPage/CnFlowEditorPage.vue +11 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +136 -7
- package/src/components/CnOrderedListField/CnOrderedListField.vue +81 -9
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +114 -7
- package/src/components/CnSettingsField/CnSettingsField.vue +449 -0
- package/src/components/CnSettingsField/index.js +4 -0
- package/src/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue +642 -0
- package/src/components/CnSettingsFieldsSection/index.js +4 -0
- package/src/components/CnSettingsPage/CnSettingsPage.vue +6 -86
- package/src/components/CnSetupWizard/CnSetupWizard.vue +37 -3
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/CnWizardDialog/CnWizardDialog.vue +7 -7
- package/src/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue +725 -0
- package/src/components/CnWorkingCalendarWidget/index.js +26 -0
- package/src/components/index.js +3 -0
- package/src/composables/useFlowStore.js +2 -1
- package/src/css/badge.css +32 -16
- package/src/css/detail-page.css +15 -0
- package/src/css/dialog.css +48 -0
- package/src/css/look-board-detail.css +80 -8
- package/src/css/look-board-index.css +52 -8
- package/src/css/look-board.css +301 -1
- package/src/index.js +3 -0
- package/src/schemas/app-manifest-v2.schema.json +25 -1
- package/src/utils/libraryWidgetKeys.js +1 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/workingCalendarMonth.js +82 -0
|
@@ -39,6 +39,15 @@
|
|
|
39
39
|
@click="$emit('run')">
|
|
40
40
|
{{ t('nextcloud-vue', 'Run') }}
|
|
41
41
|
</NcButton>
|
|
42
|
+
<NcButton v-if="testSubjectReady"
|
|
43
|
+
:disabled="store.running || !store.flow.id"
|
|
44
|
+
data-testid="flow-board-test"
|
|
45
|
+
@click="testOpen = true">
|
|
46
|
+
<template #icon>
|
|
47
|
+
<TestTube :size="16" />
|
|
48
|
+
</template>
|
|
49
|
+
{{ testLabel }}
|
|
50
|
+
</NcButton>
|
|
42
51
|
<CnBuildiqEditButton />
|
|
43
52
|
<NcButton variant="primary"
|
|
44
53
|
:disabled="store.saving || !store.flow.name || store.graphLocked"
|
|
@@ -49,6 +58,32 @@
|
|
|
49
58
|
</div>
|
|
50
59
|
</header>
|
|
51
60
|
|
|
61
|
+
<CnDialog v-if="testOpen"
|
|
62
|
+
:name="testLabel"
|
|
63
|
+
data-testid="flow-board-test-dialog"
|
|
64
|
+
@closing="testOpen = false">
|
|
65
|
+
<p class="cn-flow-board__test-intro">
|
|
66
|
+
{{ t('nextcloud-vue', 'The saved flow runs once with the record you pick as its subject.') }}
|
|
67
|
+
</p>
|
|
68
|
+
<CnResourceSelect v-model="testRecord"
|
|
69
|
+
:register="testSubject.register"
|
|
70
|
+
:schema="testSubject.schema"
|
|
71
|
+
:labelField="testSubject.labelField || 'title'"
|
|
72
|
+
:inputLabel="t('nextcloud-vue', 'Record')"
|
|
73
|
+
data-testid="flow-board-test-record" />
|
|
74
|
+
<template #actions>
|
|
75
|
+
<NcButton data-testid="flow-board-test-cancel" @click="testOpen = false">
|
|
76
|
+
{{ t('nextcloud-vue', 'Cancel') }}
|
|
77
|
+
</NcButton>
|
|
78
|
+
<NcButton variant="primary"
|
|
79
|
+
:disabled="!testRecord || store.running"
|
|
80
|
+
data-testid="flow-board-test-run"
|
|
81
|
+
@click="runTest">
|
|
82
|
+
{{ t('nextcloud-vue', 'Run test') }}
|
|
83
|
+
</NcButton>
|
|
84
|
+
</template>
|
|
85
|
+
</CnDialog>
|
|
86
|
+
|
|
52
87
|
<p class="cn-flow-board__path" data-testid="flow-board-path">
|
|
53
88
|
<a :href="listHref" class="cn-flow-board__crumb" @click="onCrumbClick($event, detailRoute)">{{ listText }}</a>
|
|
54
89
|
<span aria-hidden="true"> / </span>
|
|
@@ -186,11 +221,14 @@
|
|
|
186
221
|
<script>
|
|
187
222
|
import { translate as t } from '@nextcloud/l10n'
|
|
188
223
|
import { NcButton, NcCheckboxRadioSwitch, NcNoteCard } from '@nextcloud/vue'
|
|
224
|
+
import TestTube from 'vue-material-design-icons/TestTube.vue'
|
|
189
225
|
import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
|
|
226
|
+
import CnDialog from '../CnDialog/CnDialog.vue'
|
|
190
227
|
import CnFlowDetail from '../CnFlowDetail/CnFlowDetail.vue'
|
|
191
228
|
import CnFlowRunSummary from '../CnFlowDetail/CnFlowRunSummary.vue'
|
|
192
229
|
import CnFlowSidebar from '../CnFlowDetail/CnFlowSidebar.vue'
|
|
193
230
|
import CnFlowStepLog from '../CnFlowDetail/CnFlowStepLog.vue'
|
|
231
|
+
import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
|
|
194
232
|
import { FLOW_RUN_ACTIVE_STATUSES, useFlowStore } from '../../composables/useFlowStore.js'
|
|
195
233
|
import { formatRelativeDate } from '../../utils/boardDate.js'
|
|
196
234
|
import { flowOutcomeLevels, flowRunDurationMs, flowRunLogLines, flowRunName, formatFlowDuration } from '../../utils/flowRunLog.js'
|
|
@@ -204,7 +242,7 @@ import { CnStatusBadge } from '../CnStatusBadge/index.js'
|
|
|
204
242
|
export default {
|
|
205
243
|
name: 'CnFlowBoard',
|
|
206
244
|
|
|
207
|
-
components: { CnBuildiqEditButton, CnFlowDetail, CnFlowRunSummary, CnFlowSidebar, CnFlowStepLog, CnStatusBadge, NcButton, NcCheckboxRadioSwitch, NcNoteCard },
|
|
245
|
+
components: { CnBuildiqEditButton, CnDialog, CnFlowDetail, CnResourceSelect, TestTube, CnFlowRunSummary, CnFlowSidebar, CnFlowStepLog, CnStatusBadge, NcButton, NcCheckboxRadioSwitch, NcNoteCard },
|
|
208
246
|
|
|
209
247
|
props: {
|
|
210
248
|
/** The flow uuid, or `new`. */
|
|
@@ -225,6 +263,12 @@ export default {
|
|
|
225
263
|
notice: { type: String, default: '' },
|
|
226
264
|
/** The pill beside the title ("Case type flow"). */
|
|
227
265
|
pill: { type: String, default: '' },
|
|
266
|
+
/**
|
|
267
|
+
* The records a test run can take as its subject: `{ register, schema,
|
|
268
|
+
* labelField?, label? }`. Draws a header button ("Testen met een zaak",
|
|
269
|
+
* DqFlow) that runs the saved flow once with a picked record.
|
|
270
|
+
*/
|
|
271
|
+
testSubject: { type: Object, default: null },
|
|
228
272
|
},
|
|
229
273
|
|
|
230
274
|
emits: ['save', 'run', 'retried'],
|
|
@@ -234,10 +278,20 @@ export default {
|
|
|
234
278
|
},
|
|
235
279
|
|
|
236
280
|
data() {
|
|
237
|
-
return { runTab: 'step' }
|
|
281
|
+
return { runTab: 'step', testOpen: false, testRecord: null }
|
|
238
282
|
},
|
|
239
283
|
|
|
240
284
|
computed: {
|
|
285
|
+
/** @return {boolean} Whether a test subject is declared well enough to pick one. */
|
|
286
|
+
testSubjectReady() {
|
|
287
|
+
return Boolean(this.testSubject && this.testSubject.register && this.testSubject.schema)
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
/** @return {string} The test button's label: the page's own, else the library's. */
|
|
291
|
+
testLabel() {
|
|
292
|
+
return (this.testSubject && this.testSubject.label) || t('nextcloud-vue', 'Test with a record')
|
|
293
|
+
},
|
|
294
|
+
|
|
241
295
|
/** @return {boolean} Whether the palette card is drawn. */
|
|
242
296
|
showPalette() {
|
|
243
297
|
return this.palette === 'column' && !this.store.inRunView
|
|
@@ -454,6 +508,22 @@ export default {
|
|
|
454
508
|
},
|
|
455
509
|
|
|
456
510
|
methods: {
|
|
511
|
+
/**
|
|
512
|
+
* Run the saved flow once with the picked record as its subject, then
|
|
513
|
+
* close the dialog; the run shows on the canvas as any run does.
|
|
514
|
+
*
|
|
515
|
+
* @return {Promise<void>}
|
|
516
|
+
*/
|
|
517
|
+
async runTest() {
|
|
518
|
+
const uuid = Array.isArray(this.testRecord) ? this.testRecord[0] : this.testRecord
|
|
519
|
+
if (!uuid) {
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
await this.store.run({ uuid, register: this.testSubject.register, schema: this.testSubject.schema })
|
|
523
|
+
this.testOpen = false
|
|
524
|
+
this.testRecord = null
|
|
525
|
+
},
|
|
526
|
+
|
|
457
527
|
t,
|
|
458
528
|
|
|
459
529
|
/**
|
|
@@ -581,6 +651,11 @@ export default {
|
|
|
581
651
|
gap: 8px;
|
|
582
652
|
}
|
|
583
653
|
|
|
654
|
+
.cn-flow-board__test-intro {
|
|
655
|
+
margin: 0 0 12px;
|
|
656
|
+
color: var(--color-text-maxcontrast);
|
|
657
|
+
}
|
|
658
|
+
|
|
584
659
|
.cn-flow-board__path {
|
|
585
660
|
margin: 0;
|
|
586
661
|
color: var(--color-text-maxcontrast);
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
:paletteFootnote="paletteFootnote ? cnTranslate(paletteFootnote) : ''"
|
|
12
12
|
:notice="notice ? cnTranslate(notice) : ''"
|
|
13
13
|
:pill="pill ? cnTranslate(pill) : ''"
|
|
14
|
+
:testSubject="testSubject ? { ...testSubject, label: testSubject.label ? cnTranslate(testSubject.label) : '' } : null"
|
|
14
15
|
@save="onSave"
|
|
15
16
|
@run="onRun" />
|
|
16
17
|
<CnFlowDetail v-else
|
|
@@ -122,6 +123,16 @@ export default {
|
|
|
122
123
|
default: '',
|
|
123
124
|
},
|
|
124
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Board look: the records a test run can take as its subject,
|
|
128
|
+
* `{ register, schema, labelField?, label? }`. Manifest key
|
|
129
|
+
* `config.testSubject`; `label` is translated with the app's catalogue.
|
|
130
|
+
*/
|
|
131
|
+
testSubject: {
|
|
132
|
+
type: Object,
|
|
133
|
+
default: null,
|
|
134
|
+
},
|
|
135
|
+
|
|
125
136
|
/**
|
|
126
137
|
* Board look: an info notice under the path. Manifest key
|
|
127
138
|
* `config.notice`, translated with the app's catalogue.
|
|
@@ -1578,6 +1578,8 @@ export default {
|
|
|
1578
1578
|
*/
|
|
1579
1579
|
inject: {
|
|
1580
1580
|
cnCustomComponents: { default: () => ({}) },
|
|
1581
|
+
/** The permissions the user holds, provided by CnAppRoot; for gated page tabs. */
|
|
1582
|
+
cnPermissions: { default: () => [] },
|
|
1581
1583
|
/** The app id CnAppRoot provides, for the plural form of `countSubtitle`. */
|
|
1582
1584
|
cnAppId: { default: '' },
|
|
1583
1585
|
/**
|
|
@@ -1668,7 +1670,9 @@ export default {
|
|
|
1668
1670
|
|
|
1669
1671
|
/**
|
|
1670
1672
|
* A note after the breadcrumb and a middle dot (manifest `config.headerMeta`),
|
|
1671
|
-
* such as "{total} open": `{total}` and `{shown}` are the list's counts
|
|
1673
|
+
* such as "{total} open": `{total}` and `{shown}` are the list's counts,
|
|
1674
|
+
* and `{count:<name>}` the count of a list named in `headerMetaCounts`
|
|
1675
|
+
* ("{total} contracten bij {count:suppliers} leveranciers").
|
|
1672
1676
|
*
|
|
1673
1677
|
* @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
|
|
1674
1678
|
*/
|
|
@@ -1677,12 +1681,29 @@ export default {
|
|
|
1677
1681
|
default: '',
|
|
1678
1682
|
},
|
|
1679
1683
|
|
|
1684
|
+
/**
|
|
1685
|
+
* The other lists `headerMeta` counts (manifest `config.headerMetaCounts`):
|
|
1686
|
+
* `{ <name>: { register, schema, filter? } }`, each read as one list
|
|
1687
|
+
* total and filled in for `{count:<name>}`. While a count loads, or when
|
|
1688
|
+
* it fails, the note is left out rather than shown with a gap.
|
|
1689
|
+
*
|
|
1690
|
+
* @type {Record<string, {register: string, schema: string, filter?: object}>|null}
|
|
1691
|
+
*/
|
|
1692
|
+
headerMetaCounts: {
|
|
1693
|
+
type: Object,
|
|
1694
|
+
default: null,
|
|
1695
|
+
},
|
|
1696
|
+
|
|
1680
1697
|
/**
|
|
1681
1698
|
* Tabs to sibling pages above the toolbar (manifest `config.pageTabs`),
|
|
1682
1699
|
* each `{ id?, label, route?, params?, query?, href?, countFrom?: {
|
|
1683
|
-
* register, schema, filter? } }`: DqPrivacy's
|
|
1684
|
-
* Datalekken". The current page's tab is marked;
|
|
1685
|
-
* count of that list.
|
|
1700
|
+
* register, schema, filter? }, adminOnly?, permission? }`: DqPrivacy's
|
|
1701
|
+
* "AVG-verzoeken | Datalekken". The current page's tab is marked;
|
|
1702
|
+
* `countFrom` adds the count of that list. A tab with `adminOnly` shows
|
|
1703
|
+
* only to an instance admin, one with `permission` only to a user who
|
|
1704
|
+
* holds it. That hides the tab and nothing more: the page it points to
|
|
1705
|
+
* must still refuse the user on the server. Wins over a named source's
|
|
1706
|
+
* own page tabs.
|
|
1686
1707
|
*
|
|
1687
1708
|
* @type {Array<object>|null}
|
|
1688
1709
|
* @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs
|
|
@@ -3722,6 +3743,8 @@ export default {
|
|
|
3722
3743
|
filterStripCounts: null,
|
|
3723
3744
|
/** The `countFrom` counts of the manifest page tabs, by index. */
|
|
3724
3745
|
pageTabCounts: {},
|
|
3746
|
+
/** The `headerMetaCounts` totals, by name; a failed one is null. */
|
|
3747
|
+
headerMetaCountValues: {},
|
|
3725
3748
|
/** The tabs `quickFiltersFrom` produced, or null before they load. */
|
|
3726
3749
|
generatedQuickFilters: null,
|
|
3727
3750
|
/** Count per saved-view id, for the views control; null = none. */
|
|
@@ -6202,9 +6225,13 @@ export default {
|
|
|
6202
6225
|
if (!Array.isArray(this.pageTabs) || this.pageTabs.length === 0) {
|
|
6203
6226
|
return this.sourcePageTabs
|
|
6204
6227
|
}
|
|
6228
|
+
// The index is the tab's place in `pageTabs`, which is how its count
|
|
6229
|
+
// is keyed, so the tabs are numbered before any is left out.
|
|
6205
6230
|
return this.pageTabs
|
|
6206
|
-
.
|
|
6207
|
-
.
|
|
6231
|
+
.map((tab, index) => ({ tab, index }))
|
|
6232
|
+
.filter(({ tab }) => tab && typeof tab.label === 'string' && tab.label !== '' && (tab.route || tab.to || tab.href))
|
|
6233
|
+
.filter(({ tab }) => this.pageTabVisible(tab))
|
|
6234
|
+
.map(({ tab, index }) => {
|
|
6208
6235
|
const to = tab.route
|
|
6209
6236
|
? { name: tab.route, ...(tab.params ? { params: tab.params } : {}), ...(tab.query ? { query: tab.query } : {}) }
|
|
6210
6237
|
: (tab.to || tab.href)
|
|
@@ -6646,9 +6673,41 @@ export default {
|
|
|
6646
6673
|
return ''
|
|
6647
6674
|
}
|
|
6648
6675
|
const total = this.effectivePagination?.total
|
|
6649
|
-
|
|
6676
|
+
let missing = false
|
|
6677
|
+
const text = this.cnTranslate(headerMeta)
|
|
6650
6678
|
.replace('{shown}', String(this.effectiveObjects.length))
|
|
6651
6679
|
.replace('{total}', typeof total === 'number' ? String(total) : '')
|
|
6680
|
+
.replace(/\{count:([\w-]+)\}/g, (match, name) => {
|
|
6681
|
+
const value = this.headerMetaCountValues[name]
|
|
6682
|
+
if (typeof value !== 'number') {
|
|
6683
|
+
missing = true
|
|
6684
|
+
return ''
|
|
6685
|
+
}
|
|
6686
|
+
return String(value)
|
|
6687
|
+
})
|
|
6688
|
+
return missing ? '' : text
|
|
6689
|
+
},
|
|
6690
|
+
|
|
6691
|
+
/**
|
|
6692
|
+
* Whether the header note carries a count, so the board leaves out its
|
|
6693
|
+
* own "{shown} of {total}" line (DqPrivacy, DqContracten).
|
|
6694
|
+
*
|
|
6695
|
+
* @return {boolean}
|
|
6696
|
+
*/
|
|
6697
|
+
headerMetaCarriesCount() {
|
|
6698
|
+
const headerMeta = this.presented('headerMeta')
|
|
6699
|
+
return typeof headerMeta === 'string' && /\{(shown|total|count:[\w-]+)\}/.test(headerMeta)
|
|
6700
|
+
},
|
|
6701
|
+
|
|
6702
|
+
/**
|
|
6703
|
+
* What the header note's other counts depend on, as one key.
|
|
6704
|
+
*
|
|
6705
|
+
* @return {string}
|
|
6706
|
+
*/
|
|
6707
|
+
headerMetaCountKey() {
|
|
6708
|
+
const sources = this.headerMetaCounts && typeof this.headerMetaCounts === 'object' ? this.headerMetaCounts : {}
|
|
6709
|
+
const valid = Object.entries(sources).filter(([, from]) => from && from.register && from.schema)
|
|
6710
|
+
return valid.length > 0 ? JSON.stringify(valid) : ''
|
|
6652
6711
|
},
|
|
6653
6712
|
|
|
6654
6713
|
headerDescription() {
|
|
@@ -6661,6 +6720,10 @@ export default {
|
|
|
6661
6720
|
// reads "14 van 14" in Dutch; a page's own template goes
|
|
6662
6721
|
// through the app's label lookup.
|
|
6663
6722
|
const countText = this.presented('countText')
|
|
6723
|
+
// The note after the breadcrumb already says how many there are.
|
|
6724
|
+
if (!countText && !this.countSubtitle && this.headerMetaCarriesCount) {
|
|
6725
|
+
return this.description
|
|
6726
|
+
}
|
|
6664
6727
|
if (!countText && !this.countSubtitle) {
|
|
6665
6728
|
return t('nextcloud-vue', '{shown} of {total}', { shown, total })
|
|
6666
6729
|
}
|
|
@@ -6907,6 +6970,13 @@ export default {
|
|
|
6907
6970
|
},
|
|
6908
6971
|
},
|
|
6909
6972
|
|
|
6973
|
+
headerMetaCountKey: {
|
|
6974
|
+
immediate: true,
|
|
6975
|
+
handler() {
|
|
6976
|
+
this.loadHeaderMetaCounts()
|
|
6977
|
+
},
|
|
6978
|
+
},
|
|
6979
|
+
|
|
6910
6980
|
pageTabCountKey: {
|
|
6911
6981
|
immediate: true,
|
|
6912
6982
|
/** @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs */
|
|
@@ -7326,6 +7396,65 @@ export default {
|
|
|
7326
7396
|
]
|
|
7327
7397
|
},
|
|
7328
7398
|
|
|
7399
|
+
/**
|
|
7400
|
+
* Whether Nextcloud says the user administers this instance; false
|
|
7401
|
+
* when the user cannot be read.
|
|
7402
|
+
*
|
|
7403
|
+
* @return {boolean}
|
|
7404
|
+
*/
|
|
7405
|
+
viewerIsAdmin() {
|
|
7406
|
+
try {
|
|
7407
|
+
return getCurrentUser()?.isAdmin === true
|
|
7408
|
+
} catch {
|
|
7409
|
+
return false
|
|
7410
|
+
}
|
|
7411
|
+
},
|
|
7412
|
+
|
|
7413
|
+
/**
|
|
7414
|
+
* Whether a manifest page tab shows to this user: an `adminOnly` tab to
|
|
7415
|
+
* an instance admin, a tab with a `permission` to a user who holds it
|
|
7416
|
+
* (as a menu entry: with no permissions known, it shows). This only
|
|
7417
|
+
* decides whether the tab is drawn. It is not access control; the
|
|
7418
|
+
* page behind the tab must enforce its own access on the server.
|
|
7419
|
+
*
|
|
7420
|
+
* @param {object} tab The tab declaration.
|
|
7421
|
+
* @return {boolean} True when the tab is drawn.
|
|
7422
|
+
*/
|
|
7423
|
+
pageTabVisible(tab) {
|
|
7424
|
+
if (tab.adminOnly === true && !this.viewerIsAdmin()) {
|
|
7425
|
+
return false
|
|
7426
|
+
}
|
|
7427
|
+
if (tab.permission) {
|
|
7428
|
+
const held = Array.isArray(this.cnPermissions) ? this.cnPermissions : []
|
|
7429
|
+
if (held.length > 0 && !held.includes(tab.permission)) {
|
|
7430
|
+
return false
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
7433
|
+
return true
|
|
7434
|
+
},
|
|
7435
|
+
|
|
7436
|
+
/**
|
|
7437
|
+
* Read the totals of the lists `headerMetaCounts` names, one list
|
|
7438
|
+
* request each. A failed count is null and leaves the note out.
|
|
7439
|
+
*
|
|
7440
|
+
* @return {Promise<void>}
|
|
7441
|
+
*/
|
|
7442
|
+
async loadHeaderMetaCounts() {
|
|
7443
|
+
const key = this.headerMetaCountKey
|
|
7444
|
+
if (key === '') {
|
|
7445
|
+
this.headerMetaCountValues = {}
|
|
7446
|
+
return
|
|
7447
|
+
}
|
|
7448
|
+
const ctx = typeof this.selfFetchTokenCtx === 'function' ? this.selfFetchTokenCtx() : {}
|
|
7449
|
+
const values = {}
|
|
7450
|
+
await Promise.all(JSON.parse(key).map(async ([name, from]) => {
|
|
7451
|
+
values[name] = await fetchListTotal(from.register, from.schema, from.filter || {}, ctx).catch(() => null)
|
|
7452
|
+
}))
|
|
7453
|
+
if (key === this.headerMetaCountKey) {
|
|
7454
|
+
this.headerMetaCountValues = values
|
|
7455
|
+
}
|
|
7456
|
+
},
|
|
7457
|
+
|
|
7329
7458
|
/**
|
|
7330
7459
|
* Count the lists behind the manifest page tabs (`countFrom`), one list
|
|
7331
7460
|
* request each. A failed count shows no number.
|
|
@@ -19,7 +19,16 @@
|
|
|
19
19
|
class="cn-ordered-list-field__row"
|
|
20
20
|
:data-testid="`cn-ordered-list-row-${row.value}`">
|
|
21
21
|
<span class="cn-ordered-list-field__position" aria-hidden="true">{{ index + 1 }}</span>
|
|
22
|
-
<span class="cn-ordered-list-
|
|
22
|
+
<span v-if="row.icon" class="cn-ordered-list-field__tile" aria-hidden="true">
|
|
23
|
+
<CnIcon :name="row.icon" :size="18" />
|
|
24
|
+
</span>
|
|
25
|
+
<span class="cn-ordered-list-field__text">
|
|
26
|
+
<span class="cn-ordered-list-field__name">{{ row.label }}</span>
|
|
27
|
+
<span v-if="row.description"
|
|
28
|
+
:id="descriptionId(row)"
|
|
29
|
+
class="cn-ordered-list-field__description"
|
|
30
|
+
:data-testid="`cn-ordered-list-description-${row.value}`">{{ row.description }}</span>
|
|
31
|
+
</span>
|
|
23
32
|
<span class="cn-ordered-list-field__actions">
|
|
24
33
|
<button
|
|
25
34
|
type="button"
|
|
@@ -27,6 +36,7 @@
|
|
|
27
36
|
:disabled="disabled || index === 0"
|
|
28
37
|
:aria-label="t('nextcloud-vue', 'Move {item} up', { item: row.label })"
|
|
29
38
|
:data-testid="`cn-ordered-list-up-${row.value}`"
|
|
39
|
+
:aria-describedby="row.description ? descriptionId(row) : null"
|
|
30
40
|
@click="move(index, index - 1)">
|
|
31
41
|
<ChevronUp :size="20" />
|
|
32
42
|
</button>
|
|
@@ -36,6 +46,7 @@
|
|
|
36
46
|
:disabled="disabled || index === rows.length - 1"
|
|
37
47
|
:aria-label="t('nextcloud-vue', 'Move {item} down', { item: row.label })"
|
|
38
48
|
:data-testid="`cn-ordered-list-down-${row.value}`"
|
|
49
|
+
:aria-describedby="row.description ? descriptionId(row) : null"
|
|
39
50
|
@click="move(index, index + 1)">
|
|
40
51
|
<ChevronDown :size="20" />
|
|
41
52
|
</button>
|
|
@@ -45,6 +56,7 @@
|
|
|
45
56
|
:disabled="disabled"
|
|
46
57
|
:aria-label="t('nextcloud-vue', 'Remove {item}', { item: row.label })"
|
|
47
58
|
:data-testid="`cn-ordered-list-remove-${row.value}`"
|
|
59
|
+
:aria-describedby="row.description ? descriptionId(row) : null"
|
|
48
60
|
@click="remove(index)">
|
|
49
61
|
<Close :size="20" />
|
|
50
62
|
</button>
|
|
@@ -66,7 +78,19 @@
|
|
|
66
78
|
:filterable="!searchable"
|
|
67
79
|
data-testid="cn-ordered-list-add"
|
|
68
80
|
@search="onSearch"
|
|
69
|
-
@update:modelValue="add"
|
|
81
|
+
@update:modelValue="add">
|
|
82
|
+
<template #option="option">
|
|
83
|
+
<span class="cn-ordered-list-field__option">
|
|
84
|
+
<span v-if="option.icon" class="cn-ordered-list-field__tile" aria-hidden="true">
|
|
85
|
+
<CnIcon :name="option.icon" :size="18" />
|
|
86
|
+
</span>
|
|
87
|
+
<span class="cn-ordered-list-field__text">
|
|
88
|
+
<span class="cn-ordered-list-field__name">{{ option.label }}</span>
|
|
89
|
+
<span v-if="option.description" class="cn-ordered-list-field__description">{{ option.description }}</span>
|
|
90
|
+
</span>
|
|
91
|
+
</span>
|
|
92
|
+
</template>
|
|
93
|
+
</NcSelect>
|
|
70
94
|
</div>
|
|
71
95
|
<p class="hidden-visually" aria-live="polite" data-testid="cn-ordered-list-live">
|
|
72
96
|
{{ announcement }}
|
|
@@ -80,6 +104,7 @@ import { NcSelect } from '@nextcloud/vue'
|
|
|
80
104
|
import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
|
|
81
105
|
import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
|
|
82
106
|
import Close from 'vue-material-design-icons/Close.vue'
|
|
107
|
+
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
83
108
|
|
|
84
109
|
let orderedListUid = 0
|
|
85
110
|
|
|
@@ -109,7 +134,7 @@ let orderedListUid = 0
|
|
|
109
134
|
export default {
|
|
110
135
|
name: 'CnOrderedListField',
|
|
111
136
|
|
|
112
|
-
components: { ChevronDown, ChevronUp, Close, NcSelect },
|
|
137
|
+
components: { ChevronDown, ChevronUp, Close, CnIcon, NcSelect },
|
|
113
138
|
|
|
114
139
|
props: {
|
|
115
140
|
/** The values in their order. */
|
|
@@ -119,8 +144,11 @@ export default {
|
|
|
119
144
|
},
|
|
120
145
|
|
|
121
146
|
/**
|
|
122
|
-
* The choices, `{ value, label }` (a plain string
|
|
123
|
-
*
|
|
147
|
+
* The choices, `{ value, label, description?, icon? }` (a plain string
|
|
148
|
+
* is its own label). `description` is a muted second line ("19 open
|
|
149
|
+
* cases"), in the row and in the picker; `icon` an MDI name drawn in a
|
|
150
|
+
* small tile before the label. A value in `modelValue` without a
|
|
151
|
+
* choice shows the value itself.
|
|
124
152
|
*/
|
|
125
153
|
options: {
|
|
126
154
|
type: Array,
|
|
@@ -185,17 +213,22 @@ export default {
|
|
|
185
213
|
choices() {
|
|
186
214
|
return (Array.isArray(this.options) ? this.options : [])
|
|
187
215
|
.map((option) => (option && typeof option === 'object'
|
|
188
|
-
? {
|
|
189
|
-
|
|
216
|
+
? {
|
|
217
|
+
value: String(option.value ?? option.id ?? ''),
|
|
218
|
+
label: String(option.label ?? option.title ?? option.value ?? ''),
|
|
219
|
+
description: typeof option.description === 'string' ? option.description : '',
|
|
220
|
+
icon: typeof option.icon === 'string' ? option.icon : '',
|
|
221
|
+
}
|
|
222
|
+
: { value: String(option), label: String(option), description: '', icon: '' }))
|
|
190
223
|
.filter((option) => option.value !== '')
|
|
191
224
|
},
|
|
192
225
|
|
|
193
|
-
/** @return {Array<{value: string, label: string}>} The rows, in order. */
|
|
226
|
+
/** @return {Array<{value: string, label: string, description: string, icon: string}>} The rows, in order. */
|
|
194
227
|
rows() {
|
|
195
228
|
return (Array.isArray(this.modelValue) ? this.modelValue : []).map((value) => {
|
|
196
229
|
const key = String(value)
|
|
197
230
|
const choice = this.choices.find((option) => option.value === key)
|
|
198
|
-
return { value: key, label:
|
|
231
|
+
return choice || { value: key, label: key, description: '', icon: '' }
|
|
199
232
|
})
|
|
200
233
|
},
|
|
201
234
|
|
|
@@ -214,6 +247,16 @@ export default {
|
|
|
214
247
|
methods: {
|
|
215
248
|
t,
|
|
216
249
|
|
|
250
|
+
/**
|
|
251
|
+
* The id of a row's description, which its buttons name as their description.
|
|
252
|
+
*
|
|
253
|
+
* @param {{value: string}} row The row.
|
|
254
|
+
* @return {string}
|
|
255
|
+
*/
|
|
256
|
+
descriptionId(row) {
|
|
257
|
+
return `${this.labelId}-desc-${String(row.value).replace(/[^\w-]/g, '-')}`
|
|
258
|
+
},
|
|
259
|
+
|
|
217
260
|
/**
|
|
218
261
|
* Emit the new order.
|
|
219
262
|
*
|
|
@@ -383,4 +426,33 @@ export default {
|
|
|
383
426
|
margin: 0;
|
|
384
427
|
color: var(--color-text-maxcontrast);
|
|
385
428
|
}
|
|
429
|
+
|
|
430
|
+
.cn-ordered-list-field__tile {
|
|
431
|
+
display: inline-flex;
|
|
432
|
+
flex: none;
|
|
433
|
+
align-items: center;
|
|
434
|
+
justify-content: center;
|
|
435
|
+
width: 32px;
|
|
436
|
+
height: 32px;
|
|
437
|
+
border-radius: var(--border-radius-element, 8px);
|
|
438
|
+
background: var(--color-background-dark);
|
|
439
|
+
color: var(--color-main-text);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.cn-ordered-list-field__name {
|
|
443
|
+
display: block;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.cn-ordered-list-field__description {
|
|
447
|
+
display: block;
|
|
448
|
+
color: var(--color-text-maxcontrast);
|
|
449
|
+
font-size: 13px;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.cn-ordered-list-field__option {
|
|
453
|
+
display: flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
gap: 10px;
|
|
456
|
+
min-width: 0;
|
|
457
|
+
}
|
|
386
458
|
</style>
|