@conduction/nextcloud-vue 2.82.0 → 2.84.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/CnActionsBar/CnActionsBar.vue2.js +2 -2
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +24 -0
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +9 -4
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +125 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +14 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +203 -4
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +96 -12
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +95 -50
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js +2 -2
- package/dist/esm/components/CnMassActionBar/CnMassActionBar.vue2.js.map +1 -1
- package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js +2 -2
- package/dist/esm/components/CnMassImportDialog/CnMassImportDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +31 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +16 -2
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +3 -3
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +176 -0
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +181 -52
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +70 -5
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +61 -35
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +4 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +72 -5
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +88 -71
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +1 -1
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +18 -4
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
- 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/CnWalkthrough/CnWalkthrough.vue.js +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +16 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +41 -4
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +900 -537
- package/dist/esm/utils/buildManifest.js +37 -3
- package/dist/esm/utils/buildManifest.js.map +1 -1
- package/dist/esm/utils/detailWidgetVisibility.js +149 -0
- package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
- package/dist/esm/utils/fetchAggregate.js +25 -2
- package/dist/esm/utils/fetchAggregate.js.map +1 -1
- package/dist/esm/utils/formatMetric.js +39 -3
- package/dist/esm/utils/formatMetric.js.map +1 -1
- package/dist/esm/utils/resolveManifestTokens.js +1 -0
- package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +9 -0
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/switchValues.js +62 -0
- package/dist/esm/utils/switchValues.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +36711 -34909
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +900 -537
- package/l10n/en.json +16 -1
- package/l10n/nl.json +41 -4
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +31 -2
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
- package/src/components/CnBodySections/CnBodySections.vue +22 -1
- package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +128 -6
- package/src/components/CnDashboardPage/CnDashboardPage.vue +14 -0
- package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
- package/src/components/CnDetailPage/CnDetailPage.md +2 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
- package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +125 -15
- package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +31 -2
- package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +54 -3
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
- package/src/components/CnReportsPage/CnReportsPage.vue +263 -5
- package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +110 -18
- package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
- package/src/components/CnStatWidget/CnStatWidget.vue +4 -2
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +125 -7
- package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +18 -4
- package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
- package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
- package/src/css/form-field.css +14 -0
- package/src/css/form-widget.css +11 -0
- package/src/css/index-page.css +11 -0
- package/src/css/index.css +1 -0
- package/src/css/look-board-reports.css +175 -0
- package/src/css/look-board.css +44 -1
- package/src/dialogs/CnTransitionInputDialog.vue +87 -7
- package/src/schemas/app-manifest-v2.schema.json +24 -3
- package/src/utils/buildManifest.js +36 -2
- package/src/utils/detailWidgetVisibility.js +145 -0
- package/src/utils/fetchAggregate.js +24 -1
- package/src/utils/formatMetric.js +38 -2
- package/src/utils/resolveManifestTokens.js +1 -0
- package/src/utils/sentinelTokens.js +9 -0
- package/src/utils/switchValues.js +59 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -32,6 +32,7 @@ import { NcCheckboxRadioSwitch, NcNoteCard } from '@nextcloud/vue'
|
|
|
32
32
|
import { objectUrlOf, writeFields } from '../../utils/objectWrite.js'
|
|
33
33
|
import { readPath } from '../../utils/readPath.js'
|
|
34
34
|
import { serverErrorMessage } from '../../utils/serverErrorMessage.js'
|
|
35
|
+
import { switchIsOn, switchStoredValue } from '../../utils/switchValues.js'
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* CnSwitchRowsWidget — the `switch-rows` widget: a card of switches, each with
|
|
@@ -54,7 +55,8 @@ export default {
|
|
|
54
55
|
/**
|
|
55
56
|
* The widget configuration.
|
|
56
57
|
*
|
|
57
|
-
* - `rows` (required) — `[{ key, label, hint?, invert? }]`; `key` is a boolean field, a top-level name or a dot-path. `invert` shows the switch on when the field is false (a stored `disabled` read as "enabled").
|
|
58
|
+
* - `rows` (required) — `[{ key, label, hint?, invert?, values? }]`; `key` is a boolean field, a top-level name or a dot-path. `invert` shows the switch on when the field is false (a stored `disabled` read as "enabled"). `values: { on, off }` reads and writes those values instead of true/false (an app config flag stored as `"1"`/`"0"`).
|
|
59
|
+
* - `values` — `{ on, off }` for every row that declares none.
|
|
58
60
|
* - `save` — `{ url, method?, payload? }`, write through an app endpoint (tokens `{value}`, `{field}`, `{changes}`, `{object.<path>}`).
|
|
59
61
|
* - `readOnly`.
|
|
60
62
|
*
|
|
@@ -137,11 +139,22 @@ export default {
|
|
|
137
139
|
return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
|
|
138
140
|
},
|
|
139
141
|
|
|
142
|
+
/**
|
|
143
|
+
* A row's value map: its own `values`, else the widget's.
|
|
144
|
+
*
|
|
145
|
+
* @spec openspec/changes/screens-switch-value-map/specs/switch-rows/spec.md#requirement-a-switch-can-read-and-write-a-value-map
|
|
146
|
+
* @param {object} row The row.
|
|
147
|
+
* @return {object|undefined} The declared `values`.
|
|
148
|
+
*/
|
|
149
|
+
valuesOf(row) {
|
|
150
|
+
return row.values !== undefined ? row.values : this.content.values
|
|
151
|
+
},
|
|
152
|
+
|
|
140
153
|
/** Read every switch off the object. */
|
|
141
154
|
adopt() {
|
|
142
155
|
const values = {}
|
|
143
156
|
for (const row of this.rows) {
|
|
144
|
-
const stored = readPath(this.objectData || {}, row.key)
|
|
157
|
+
const stored = switchIsOn(readPath(this.objectData || {}, row.key), this.valuesOf(row))
|
|
145
158
|
values[row.key] = row.invert ? !stored : stored
|
|
146
159
|
}
|
|
147
160
|
this.saved = values
|
|
@@ -165,10 +178,11 @@ export default {
|
|
|
165
178
|
this.busy = row.key
|
|
166
179
|
this.values = { ...this.values, [row.key]: on }
|
|
167
180
|
const stored = row.invert ? !on : on
|
|
181
|
+
const values = this.valuesOf(row)
|
|
168
182
|
try {
|
|
169
|
-
const data = await writeFields(this.objectUrl, { [row.key]: stored }, { save: this.content.save, object: this.objectData })
|
|
183
|
+
const data = await writeFields(this.objectUrl, { [row.key]: switchStoredValue(stored, values) }, { save: this.content.save, object: this.objectData })
|
|
170
184
|
const answered = readPath(data, row.key)
|
|
171
|
-
const written = answered !== undefined ? answered
|
|
185
|
+
const written = answered !== undefined ? switchIsOn(answered, values) : stored
|
|
172
186
|
const shown = row.invert ? !written : written
|
|
173
187
|
this.saved = { ...this.saved, [row.key]: shown }
|
|
174
188
|
this.values = { ...this.values, [row.key]: shown }
|
|
@@ -311,6 +311,12 @@ export default {
|
|
|
311
311
|
cnLook: { default: 'nextcloud' },
|
|
312
312
|
/** The tab list's name under the board look, provided by CnDetailPage (`config.tabsLabel`). */
|
|
313
313
|
cnDetailTabsLabel: { default: null },
|
|
314
|
+
/**
|
|
315
|
+
* The records the detail page's `visibilityContext` names, keyed
|
|
316
|
+
* by name (a case's case type), provided by CnDetailPage. A tab's
|
|
317
|
+
* `visibleWhen` reads them as `@context.<name>`. Null on any other page.
|
|
318
|
+
*/
|
|
319
|
+
cnDetailVisibilityRows: { default: null },
|
|
314
320
|
/**
|
|
315
321
|
* The host's translate (CnAppRoot provides `cnTranslate`), so a tab
|
|
316
322
|
* label written in the manifest reads in the user's language.
|
|
@@ -475,6 +481,29 @@ export default {
|
|
|
475
481
|
},
|
|
476
482
|
|
|
477
483
|
computed: {
|
|
484
|
+
/**
|
|
485
|
+
* What a tab's `visibleWhen` reads: the record, with the records the
|
|
486
|
+
* detail page's `visibilityContext` names under `@context.<name>`. Null while
|
|
487
|
+
* the record, or one of those records, is not there yet.
|
|
488
|
+
*
|
|
489
|
+
* @return {object|null}
|
|
490
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
491
|
+
*/
|
|
492
|
+
conditionData() {
|
|
493
|
+
if (!this.objectData) {
|
|
494
|
+
return null
|
|
495
|
+
}
|
|
496
|
+
const holder = this.cnDetailVisibilityRows
|
|
497
|
+
if (!holder) {
|
|
498
|
+
return this.objectData
|
|
499
|
+
}
|
|
500
|
+
const rows = holder.value
|
|
501
|
+
if (!rows) {
|
|
502
|
+
return null
|
|
503
|
+
}
|
|
504
|
+
return { ...this.objectData, '@context': rows }
|
|
505
|
+
},
|
|
506
|
+
|
|
478
507
|
/**
|
|
479
508
|
* The open panel's own menu items, or an empty list.
|
|
480
509
|
*
|
|
@@ -859,10 +888,11 @@ export default {
|
|
|
859
888
|
return 'visible'
|
|
860
889
|
}
|
|
861
890
|
if (isLocallyDecidableVisibleWhen(cond)) {
|
|
862
|
-
|
|
891
|
+
const data = this.conditionData
|
|
892
|
+
if (!data) {
|
|
863
893
|
return 'pending'
|
|
864
894
|
}
|
|
865
|
-
return evaluateVisibleWhenLocal(cond,
|
|
895
|
+
return evaluateVisibleWhenLocal(cond, data) ? 'visible' : 'hidden'
|
|
866
896
|
}
|
|
867
897
|
const answer = this.sourceVisibility[index]
|
|
868
898
|
if (answer === undefined) {
|
|
@@ -883,7 +913,7 @@ export default {
|
|
|
883
913
|
async refreshSourceVisibility() {
|
|
884
914
|
const tabs = Array.isArray(this.content?.tabs) ? this.content.tabs : []
|
|
885
915
|
const seq = ++this.visibilitySeq
|
|
886
|
-
const ctx = { objectId: this.objectId, object: this.objectData || undefined }
|
|
916
|
+
const ctx = { objectId: this.objectId, object: this.conditionData || this.objectData || undefined }
|
|
887
917
|
await Promise.all(tabs.map(async (tab, index) => {
|
|
888
918
|
const cond = tab && typeof tab === 'object' ? tab.visibleWhen : null
|
|
889
919
|
if (!cond || isLocallyDecidableVisibleWhen(cond)) {
|
|
@@ -683,11 +683,27 @@ export default {
|
|
|
683
683
|
* deferred tour) go through here — so a centered step cannot be
|
|
684
684
|
* swallowed by either.
|
|
685
685
|
*
|
|
686
|
+
* A tour that declares `startOn` opens only on one of those route
|
|
687
|
+
* names, whatever its first step looks like.
|
|
688
|
+
*
|
|
686
689
|
* @param {object} tour The tour definition.
|
|
687
690
|
* @param {string} [routeName] The current route name.
|
|
688
691
|
* @return {boolean} True when the tour may open on this route.
|
|
692
|
+
* @spec openspec/changes/a-tour-starts-on-the-pages-it-names/specs/walkthrough/spec.md
|
|
689
693
|
*/
|
|
690
694
|
routeMatchesTour(tour, routeName) {
|
|
695
|
+
// A tour that names where it starts (`startOn`) opens only there.
|
|
696
|
+
// It wins over the centered-step rule below: a welcome card floats
|
|
697
|
+
// in the middle of the screen, but a user who opens a case from a
|
|
698
|
+
// mail link lands on that case, and the card then blocks the page
|
|
699
|
+
// they came for. Measured 13 October on dossiq: a staff user's first
|
|
700
|
+
// visit was a deep link to a Woo request, and "Welcome to Dossiq"
|
|
701
|
+
// opened over it. The tour waits in `_pendingAutoTour` and opens when
|
|
702
|
+
// the user reaches a named route.
|
|
703
|
+
const startOn = Array.isArray(tour && tour.startOn) ? tour.startOn.map(String) : []
|
|
704
|
+
if (startOn.length > 0) {
|
|
705
|
+
return !!routeName && startOn.includes(String(routeName))
|
|
706
|
+
}
|
|
691
707
|
const page = this.firstStepPage(tour)
|
|
692
708
|
if (!page) {
|
|
693
709
|
return true
|
package/src/css/form-field.css
CHANGED
|
@@ -183,3 +183,17 @@
|
|
|
183
183
|
.cn-look-board .cn-form-page__footer .cn-form-page__primary {
|
|
184
184
|
margin-inline-start: auto;
|
|
185
185
|
}
|
|
186
|
+
|
|
187
|
+
/* A Nextcloud text area or text field with its label inside the field hides
|
|
188
|
+
its placeholder until the field takes focus, so the label and the
|
|
189
|
+
placeholder are never drawn over each other. A theme that forces every
|
|
190
|
+
placeholder visible (thematiq's nldesign system sets
|
|
191
|
+
`::placeholder { opacity: 1 !important }`) drew both: the dossiq DqTaak
|
|
192
|
+
notes field read "Een notitie toevoegen" over "Wat heb je gevonden, wie heb
|
|
193
|
+
je gebeld?". Under the board look the placeholder stays hidden at rest, as
|
|
194
|
+
Nextcloud intends; a field with its label outside keeps its placeholder.
|
|
195
|
+
@spec openspec/changes/board-field-label-over-placeholder/specs/form-field-board-look/spec.md#requirement-a-field-label-and-its-placeholder-are-never-drawn-over-each-other */
|
|
196
|
+
.cn-look-board .textarea__input:not(:focus, .textarea__input--label-outside)::placeholder,
|
|
197
|
+
.cn-look-board .input-field:not(.input-field--label-outside) .input-field__input:not(:focus)::placeholder {
|
|
198
|
+
opacity: 0 !important;
|
|
199
|
+
}
|
package/src/css/form-widget.css
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
.cn-form-widget__textarea {
|
|
40
|
+
box-sizing: border-box;
|
|
40
41
|
width: 100%;
|
|
41
42
|
min-height: 80px;
|
|
42
43
|
padding: 8px;
|
|
@@ -58,3 +59,13 @@
|
|
|
58
59
|
color: var(--color-error);
|
|
59
60
|
margin: 0;
|
|
60
61
|
}
|
|
62
|
+
|
|
63
|
+
/* A select takes its field's width, however narrow the card. NcSelect is at
|
|
64
|
+
least 260px wide on its own, so in pipelinq's 234px "Nieuw contactmoment"
|
|
65
|
+
card at 1100 wide the channel and client selects ran 40px past the card's
|
|
66
|
+
edge. The client picker (CnResourceSelect) is an NcSelect too.
|
|
67
|
+
@spec openspec/changes/interaction-form-fits-and-reads-dutch/specs/interaction-form-widget/spec.md#requirement-the-interaction-form-fits-its-card-and-reads-in-the-readers-language */
|
|
68
|
+
.cn-form-widget .v-select.select {
|
|
69
|
+
min-width: 0;
|
|
70
|
+
width: 100%;
|
|
71
|
+
}
|
package/src/css/index-page.css
CHANGED
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
padding: calc(5 * var(--default-grid-baseline));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/* A group heading in the header's Actions menu (`{ heading }` in items), as
|
|
14
|
+
the board's "Ga naar" and "Selectie": 12px bold capitals, muted. The menu
|
|
15
|
+
is a popover outside the page, so the class is not under the look class. */
|
|
16
|
+
.cn-index-header-menu__heading {
|
|
17
|
+
font-size: 12px;
|
|
18
|
+
font-weight: 700;
|
|
19
|
+
letter-spacing: 0.04em;
|
|
20
|
+
text-transform: uppercase;
|
|
21
|
+
color: var(--color-text-maxcontrast);
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
/* The board's header buttons (`headerButtons`) at the end of the title row,
|
|
14
25
|
centred on the title and its count line. They wrap under the title on a
|
|
15
26
|
narrow page. */
|
package/src/css/index.css
CHANGED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The reports page under the board look (screens-reports-page-board-look,
|
|
3
|
+
* board DcRapportages): a 28px title over a muted lead, one section per
|
|
4
|
+
* category headed at 18px, and two-up cards with an icon tile. Scoped under
|
|
5
|
+
* `.cn-look-board` with the page guard, so an app without the look renders as
|
|
6
|
+
* before.
|
|
7
|
+
*/
|
|
8
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
gap: var(--cn-board-section-gap);
|
|
13
|
+
padding: var(--cn-board-content-padding);
|
|
14
|
+
max-width: var(--cn-board-content-max-width);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__header {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: 14px;
|
|
21
|
+
padding: 4px 0 8px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* The 56px inset clears the navigation toggle only while the navigation is
|
|
25
|
+
closed, as on the other board pages. */
|
|
26
|
+
.cn-look-board:has(.app-navigation--closed) .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__header {
|
|
27
|
+
padding-inline-start: 56px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__title {
|
|
31
|
+
margin: 0;
|
|
32
|
+
font-size: 28px;
|
|
33
|
+
font-weight: 700;
|
|
34
|
+
line-height: 1.2;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__description {
|
|
38
|
+
margin: 0;
|
|
39
|
+
font-size: 14px;
|
|
40
|
+
color: var(--color-text-maxcontrast);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__section {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
gap: 12px;
|
|
47
|
+
/* A host's own `section` rule (a width cap, a margin) does not reach it. */
|
|
48
|
+
max-width: none;
|
|
49
|
+
margin: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__section-title {
|
|
53
|
+
margin: 0;
|
|
54
|
+
font-size: 18px;
|
|
55
|
+
font-weight: 700;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__grid {
|
|
59
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
60
|
+
gap: 14px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card {
|
|
64
|
+
box-sizing: border-box;
|
|
65
|
+
flex-direction: row;
|
|
66
|
+
align-items: flex-start;
|
|
67
|
+
gap: 14px;
|
|
68
|
+
padding: 18px 20px;
|
|
69
|
+
border: 1px solid var(--color-border);
|
|
70
|
+
border-radius: var(--cn-board-card-radius);
|
|
71
|
+
background: var(--color-main-background);
|
|
72
|
+
color: var(--color-main-text);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card:hover,
|
|
76
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card:focus-visible {
|
|
77
|
+
background: var(--color-background-hover);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card-icon {
|
|
81
|
+
flex: none;
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
width: 44px;
|
|
86
|
+
height: 44px;
|
|
87
|
+
border-radius: 10px;
|
|
88
|
+
background: var(--color-primary-element-light);
|
|
89
|
+
color: var(--color-primary-element-light-text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card-text {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 4px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card-title {
|
|
99
|
+
font-size: 17px;
|
|
100
|
+
font-weight: 700;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card-description {
|
|
104
|
+
font-size: 14px;
|
|
105
|
+
line-height: 1.5;
|
|
106
|
+
color: var(--color-text-maxcontrast);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* The widgets under the cards sit in the page's own padding: the dashboard
|
|
110
|
+
inside draws no second frame. */
|
|
111
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__widgets {
|
|
112
|
+
margin-top: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-dashboard-page {
|
|
116
|
+
padding: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* The widget area draws no panel of its own (board DcRapportages: the
|
|
120
|
+
highlighted report's card sits straight on the page ground). A theme's
|
|
121
|
+
catch-all for anything named like a widget (nldesign's element overrides:
|
|
122
|
+
`[class*="widget"]:not([class*="widget__"]):not(#header *)`, an id
|
|
123
|
+
specificity no class rule outranks) painted `__widgets` white, hence the
|
|
124
|
+
`!important`. */
|
|
125
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__widgets {
|
|
126
|
+
background: transparent !important;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* The grid insets every card by its 12px margin (CnDashboardPage's default
|
|
130
|
+
`gridMargin`); the page padding already places the area, so the card's
|
|
131
|
+
edges line up with the report cards above it. */
|
|
132
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__widgets .cn-dashboard-grid {
|
|
133
|
+
width: calc(100% + 24px);
|
|
134
|
+
margin: -12px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* A report card is a link, but its title and description read as text, as on
|
|
138
|
+
the board: no underline, whatever a theme does to links. */
|
|
139
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card,
|
|
140
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card * {
|
|
141
|
+
text-decoration: none !important;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* The header buttons beside the title (screens-reports-header-buttons, board
|
|
145
|
+
DcRapportages "Downloaden"): the board's 40px button with a grey border on
|
|
146
|
+
the main background, the primary one in the primary fill, as on the index
|
|
147
|
+
header. `!important` holds them under a theme that restyles every button
|
|
148
|
+
(nldesign). The buildiq square keeps its own rule. */
|
|
149
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__header-buttons > .button-vue--secondary {
|
|
150
|
+
height: var(--cn-board-control-height) !important;
|
|
151
|
+
min-height: var(--cn-board-control-height) !important;
|
|
152
|
+
padding: 0 14px !important;
|
|
153
|
+
border: 1px solid var(--color-border-dark) !important;
|
|
154
|
+
border-radius: var(--cn-board-control-radius) !important;
|
|
155
|
+
background: var(--color-main-background) !important;
|
|
156
|
+
color: var(--color-main-text) !important;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__header-buttons > .button-vue--secondary:hover {
|
|
160
|
+
background: var(--color-background-hover) !important;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__header-buttons > .button-vue--primary {
|
|
164
|
+
height: var(--cn-board-control-height) !important;
|
|
165
|
+
min-height: var(--cn-board-control-height) !important;
|
|
166
|
+
padding: 0 16px !important;
|
|
167
|
+
border: 0 !important;
|
|
168
|
+
border-radius: var(--cn-board-control-radius) !important;
|
|
169
|
+
background: var(--color-primary-element) !important;
|
|
170
|
+
color: var(--color-primary-element-text) !important;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__header-buttons > .button-vue--primary:hover {
|
|
174
|
+
background: var(--color-primary-element-hover) !important;
|
|
175
|
+
}
|
package/src/css/look-board.css
CHANGED
|
@@ -436,9 +436,20 @@
|
|
|
436
436
|
align-items: start;
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
-
/*
|
|
439
|
+
/* An app component that wraps its sections in one element (decidiq's
|
|
440
|
+
Settings.vue) takes no box of its own: its sections are grid items and
|
|
441
|
+
cards, as if the shell held them directly. Only an element whose own
|
|
442
|
+
children are sections; anything else keeps spanning the grid. */
|
|
443
|
+
.cn-look-board .cn-admin-settings-shell:not(.cn-look-nextcloud *) > :not(.settings-section):has(> .settings-section),
|
|
444
|
+
.cn-look-board.cn-admin-settings-shell:not(.cn-look-nextcloud *) > :not(.settings-section):has(> .settings-section) {
|
|
445
|
+
display: contents;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* The card: a section of the shell (also one inside a wrapping element), or a CnSettingsCard of the page. */
|
|
440
449
|
.cn-look-board .cn-admin-settings-shell:not(.cn-look-nextcloud *) > .settings-section,
|
|
441
450
|
.cn-look-board.cn-admin-settings-shell:not(.cn-look-nextcloud *) > .settings-section,
|
|
451
|
+
.cn-look-board .cn-admin-settings-shell:not(.cn-look-nextcloud *) > :not(.settings-section):has(> .settings-section) > .settings-section,
|
|
452
|
+
.cn-look-board.cn-admin-settings-shell:not(.cn-look-nextcloud *) > :not(.settings-section):has(> .settings-section) > .settings-section,
|
|
442
453
|
.cn-look-board .cn-settings-page__sections:not(.cn-look-nextcloud *) > .cn-settings-card {
|
|
443
454
|
box-sizing: border-box;
|
|
444
455
|
display: flex;
|
|
@@ -465,7 +476,12 @@
|
|
|
465
476
|
display: none;
|
|
466
477
|
}
|
|
467
478
|
|
|
479
|
+
/* NcSettingsSection draws its name `display: inline-flex; justify-content:
|
|
480
|
+
center`; in the card's flex column the name spans the card, so the title
|
|
481
|
+
sat centred. The board's card titles start at the left edge. */
|
|
468
482
|
.cn-look-board .settings-section:not(.cn-look-nextcloud *) .settings-section__name {
|
|
483
|
+
justify-content: flex-start;
|
|
484
|
+
text-align: start;
|
|
469
485
|
margin: 0;
|
|
470
486
|
font-size: 19px;
|
|
471
487
|
font-weight: 700;
|
|
@@ -600,6 +616,18 @@
|
|
|
600
616
|
font-weight: 600;
|
|
601
617
|
}
|
|
602
618
|
|
|
619
|
+
/* ---- Dashboard card titles --------------------------------------------- */
|
|
620
|
+
/* A card's title on a dashboard (and on the widgets under a reports page) is
|
|
621
|
+
the board's card h2: 17px weight 700 (boards PqMijnWerk, DcRapportages and
|
|
622
|
+
the app dashboards; `--cn-board-heading-size`, as on a detail page's cards).
|
|
623
|
+
The wrapper's own 14px weight 600 stays for the Nextcloud look, and the
|
|
624
|
+
native-dashboard chrome keeps its 20px. */
|
|
625
|
+
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--nc-dashboard) .cn-widget-wrapper__title {
|
|
626
|
+
font-size: var(--cn-board-heading-size, 17px);
|
|
627
|
+
font-weight: 700;
|
|
628
|
+
line-height: 1.3;
|
|
629
|
+
}
|
|
630
|
+
|
|
603
631
|
/* ---- Record list on a dashboard card (PtDashboard) ---------------------- */
|
|
604
632
|
/* The dashboard's card body is flush, so the rows carry their own inset: the
|
|
605
633
|
board's card pads 24px at the sides and each row 14px, a soft hairline
|
|
@@ -667,6 +695,21 @@
|
|
|
667
695
|
margin-inline-start: auto;
|
|
668
696
|
}
|
|
669
697
|
|
|
698
|
+
/* The title and meta take the room the pill and the date leave, and a long
|
|
699
|
+
title wraps inside it: the pill and the date stay on the title line, as the
|
|
700
|
+
board draws them. They drop under the text only when the card is too narrow
|
|
701
|
+
to hold 240px of text beside them (PqMijnWerk). */
|
|
702
|
+
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__main {
|
|
703
|
+
flex: 1 1 240px;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/* ---- Saved views on a dashboard card (PqMijnWerk) ------------------------ */
|
|
707
|
+
/* The card body is flush, so the list and its empty line carry the card's
|
|
708
|
+
inset themselves, the same 24px at the sides as a record list. */
|
|
709
|
+
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-saved-views-widget {
|
|
710
|
+
padding: 4px 24px 20px;
|
|
711
|
+
}
|
|
712
|
+
|
|
670
713
|
/* The board draws no chevron after the pill and date. */
|
|
671
714
|
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__chevron {
|
|
672
715
|
display: none;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
@closing="$emit('close')">
|
|
15
15
|
<div
|
|
16
16
|
class="cn-transition-input"
|
|
17
|
+
:aria-busy="busy ? 'true' : null"
|
|
17
18
|
data-testid="cn-modal"
|
|
18
19
|
data-testid-modal="cn-transition-input-dialog">
|
|
19
20
|
<!-- A refusal keeps this dialog open with what was typed still in
|
|
@@ -29,8 +30,18 @@
|
|
|
29
30
|
class="cn-transition-input__field"
|
|
30
31
|
:class="{ 'cn-transition-input__field--refused': isRefused(field.key) }"
|
|
31
32
|
:data-testid="`cn-transition-input-${field.key}`">
|
|
33
|
+
<!-- An input that declares its choices is picked from them. -->
|
|
34
|
+
<NcSelect v-if="field.widget === 'select'"
|
|
35
|
+
:modelValue="selectedOption(field)"
|
|
36
|
+
:options="field.options"
|
|
37
|
+
label="label"
|
|
38
|
+
:inputLabel="requiredLabel(field)"
|
|
39
|
+
:clearable="!field.required"
|
|
40
|
+
:data-testid="`cn-transition-input-select-${field.key}`"
|
|
41
|
+
@update:modelValue="setValue(field.key, $event && $event.value !== undefined ? $event.value : '')" />
|
|
42
|
+
|
|
32
43
|
<!-- An input that points at another record is picked, never typed. -->
|
|
33
|
-
<div v-if="field.widget === 'reference'" class="cn-transition-input__reference">
|
|
44
|
+
<div v-else-if="field.widget === 'reference'" class="cn-transition-input__reference">
|
|
34
45
|
<label class="cn-transition-input__label">{{ requiredLabel(field) }}</label>
|
|
35
46
|
<CnResourceSelect
|
|
36
47
|
:register="field.referenceRegister"
|
|
@@ -128,11 +139,18 @@
|
|
|
128
139
|
@click="$emit('close')">
|
|
129
140
|
{{ t('nextcloud-vue', 'Cancel') }}
|
|
130
141
|
</NcButton>
|
|
142
|
+
<!-- Busy while the parent's POST runs: a transition can take
|
|
143
|
+
seconds, and a confirm button that only greys out reads as one
|
|
144
|
+
that did nothing. -->
|
|
131
145
|
<NcButton
|
|
132
146
|
variant="primary"
|
|
133
147
|
:disabled="!canConfirm"
|
|
148
|
+
:aria-busy="busy ? 'true' : null"
|
|
134
149
|
data-testid="cn-transition-input-confirm"
|
|
135
150
|
@click="onConfirm">
|
|
151
|
+
<template v-if="busy" #icon>
|
|
152
|
+
<NcLoadingIcon :size="20" data-testid="cn-transition-input-busy" />
|
|
153
|
+
</template>
|
|
136
154
|
{{ confirmLabel }}
|
|
137
155
|
</NcButton>
|
|
138
156
|
</template>
|
|
@@ -141,7 +159,7 @@
|
|
|
141
159
|
|
|
142
160
|
<script>
|
|
143
161
|
import { translate as t } from '@nextcloud/l10n'
|
|
144
|
-
import { NcButton, NcCheckboxRadioSwitch, NcTextArea, NcTextField } from '@nextcloud/vue'
|
|
162
|
+
import { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcSelect, NcTextArea, NcTextField } from '@nextcloud/vue'
|
|
145
163
|
import CnDialog from '../components/CnDialog/CnDialog.vue'
|
|
146
164
|
import CnResourceSelect from '../components/CnResourceSelect/CnResourceSelect.vue'
|
|
147
165
|
import { dialogBoardMixin } from '../mixins/dialogBoard.js'
|
|
@@ -164,7 +182,13 @@ import { fieldsFromSchema } from '../utils/schema.js'
|
|
|
164
182
|
* from the property `title`, booleans render as a switch, numbers as a number
|
|
165
183
|
* field, and long text (`maxLength > 255` or a `textarea`/`markdown` format)
|
|
166
184
|
* as a textarea. Anything else — including a field the schema does not
|
|
167
|
-
* declare — falls back to a plain labelled text input.
|
|
185
|
+
* declare — falls back to a plain labelled text input.
|
|
186
|
+
*
|
|
187
|
+
* An input may also say how it is asked for, which matters when the choices
|
|
188
|
+
* depend on the record rather than on its schema: `label` names the field,
|
|
189
|
+
* and `options: [{ value, label }]` turns it into a picker that shows each
|
|
190
|
+
* `label` and sends the chosen `value`. An input that declares neither is
|
|
191
|
+
* rendered from the schema as before. Deliberately minimal:
|
|
168
192
|
* text, textarea, number and checkbox cover transition inputs; this is not a
|
|
169
193
|
* form engine (use `CnFormDialog` for full object forms).
|
|
170
194
|
*
|
|
@@ -186,9 +210,11 @@ export default {
|
|
|
186
210
|
CnResourceSelect,
|
|
187
211
|
CnDialog,
|
|
188
212
|
NcButton,
|
|
213
|
+
NcLoadingIcon,
|
|
189
214
|
NcTextField,
|
|
190
215
|
NcTextArea,
|
|
191
216
|
NcCheckboxRadioSwitch,
|
|
217
|
+
NcSelect,
|
|
192
218
|
},
|
|
193
219
|
|
|
194
220
|
mixins: [dialogBoardMixin],
|
|
@@ -199,7 +225,11 @@ export default {
|
|
|
199
225
|
* `x-openregister-lifecycle.transitions.<action>.inputs` (server-derived
|
|
200
226
|
* or config-declared); `label` doubles as dialog title + confirm label.
|
|
201
227
|
*
|
|
202
|
-
*
|
|
228
|
+
* An input may carry a `label` for its field and `options` to pick
|
|
229
|
+
* from, each `{ value, label }`; the picker shows the label and sends
|
|
230
|
+
* the value.
|
|
231
|
+
*
|
|
232
|
+
* @type {{action?: string, label?: string, inputs?: Array<{field: string, required?: boolean, label?: string, options?: Array<{value: string, label?: string}>}>}}
|
|
203
233
|
*/
|
|
204
234
|
transition: {
|
|
205
235
|
type: Object,
|
|
@@ -296,6 +326,8 @@ export default {
|
|
|
296
326
|
required: input.required === true,
|
|
297
327
|
picker: input.picker && typeof input.picker === 'object' ? input.picker : null,
|
|
298
328
|
fields: Array.isArray(input.fields) ? input.fields.filter((f) => typeof f === 'string') : null,
|
|
329
|
+
label: typeof input.label === 'string' && input.label.trim() !== '' ? input.label.trim() : null,
|
|
330
|
+
options: this.declaredOptions(input.options),
|
|
299
331
|
}))
|
|
300
332
|
},
|
|
301
333
|
|
|
@@ -305,26 +337,40 @@ export default {
|
|
|
305
337
|
* widget from the type/format); the widget is then clamped to the minimal
|
|
306
338
|
* set this dialog renders (text / textarea / number / checkbox). Fields
|
|
307
339
|
* the schema does not declare fall back to a plain text input.
|
|
340
|
+
*
|
|
341
|
+
* An input that declares `options` is a picker whatever the schema
|
|
342
|
+
* says, because its choices are the ones this transition offers on
|
|
343
|
+
* this record. A declared `label` names the field over the schema's.
|
|
308
344
|
*/
|
|
309
345
|
fields() {
|
|
310
346
|
const keys = this.inputs.map((input) => input.field)
|
|
311
347
|
const resolved = fieldsFromSchema(this.schema, { include: keys, includeReadOnly: true })
|
|
312
348
|
return this.inputs.map((input) => {
|
|
313
349
|
const field = resolved.find((f) => f.key === input.field)
|
|
350
|
+
if (input.options) {
|
|
351
|
+
return {
|
|
352
|
+
key: input.field,
|
|
353
|
+
label: input.label || (field && field.label) || input.field,
|
|
354
|
+
description: (field && field.description) || '',
|
|
355
|
+
widget: 'select',
|
|
356
|
+
options: input.options,
|
|
357
|
+
required: input.required,
|
|
358
|
+
}
|
|
359
|
+
}
|
|
314
360
|
const prop = (this.schema && this.schema.properties && this.schema.properties[input.field]) || null
|
|
315
361
|
if (prop && prop.type === 'object' && prop.properties && typeof prop.properties === 'object') {
|
|
316
362
|
return this.objectField(input, prop)
|
|
317
363
|
}
|
|
318
364
|
if (!field) {
|
|
319
|
-
return { key: input.field, label: input.field, description: '', widget: 'text', required: input.required }
|
|
365
|
+
return { key: input.field, label: input.label || input.field, description: '', widget: 'text', required: input.required }
|
|
320
366
|
}
|
|
321
367
|
if (field.reference && field.reference.multiple !== true) {
|
|
322
368
|
const register = field.reference.register || this.register
|
|
323
369
|
if (register !== '') {
|
|
324
|
-
return { ...field, widget: 'reference', referenceRegister: register, picker: input.picker, required: input.required }
|
|
370
|
+
return { ...field, label: input.label || field.label, widget: 'reference', referenceRegister: register, picker: input.picker, required: input.required }
|
|
325
371
|
}
|
|
326
372
|
}
|
|
327
|
-
return { ...field, widget: this.clampWidget(field.widget), required: input.required }
|
|
373
|
+
return { ...field, label: input.label || field.label, widget: this.clampWidget(field.widget), required: input.required }
|
|
328
374
|
})
|
|
329
375
|
},
|
|
330
376
|
|
|
@@ -438,6 +484,40 @@ export default {
|
|
|
438
484
|
}
|
|
439
485
|
},
|
|
440
486
|
|
|
487
|
+
/**
|
|
488
|
+
* The choices an input declares, as `[{ value, label }]`, or null when
|
|
489
|
+
* it declares none. An option without a value is dropped, because it
|
|
490
|
+
* cannot be sent; one without a label shows its value.
|
|
491
|
+
*
|
|
492
|
+
* @param {unknown} options The input's declared `options`.
|
|
493
|
+
* @return {Array<{value: string, label: string}>|null}
|
|
494
|
+
*/
|
|
495
|
+
declaredOptions(options) {
|
|
496
|
+
if (!Array.isArray(options)) {
|
|
497
|
+
return null
|
|
498
|
+
}
|
|
499
|
+
const usable = options
|
|
500
|
+
.filter((option) => option && typeof option === 'object'
|
|
501
|
+
&& (typeof option.value === 'string' || typeof option.value === 'number')
|
|
502
|
+
&& String(option.value).trim() !== '')
|
|
503
|
+
.map((option) => ({
|
|
504
|
+
value: String(option.value),
|
|
505
|
+
label: typeof option.label === 'string' && option.label.trim() !== '' ? option.label.trim() : String(option.value),
|
|
506
|
+
}))
|
|
507
|
+
return usable.length > 0 ? usable : null
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* The option a picker field currently holds, or null.
|
|
512
|
+
*
|
|
513
|
+
* @param {object} field The select field descriptor.
|
|
514
|
+
* @return {{value: string, label: string}|null}
|
|
515
|
+
*/
|
|
516
|
+
selectedOption(field) {
|
|
517
|
+
const value = this.values[field.key]
|
|
518
|
+
return field.options.find((option) => option.value === String(value ?? '')) || null
|
|
519
|
+
},
|
|
520
|
+
|
|
441
521
|
/**
|
|
442
522
|
* Ids a reference picker must not offer: the current record, when the
|
|
443
523
|
* input's `picker.excludeSelf` says so.
|