@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
|
@@ -183,7 +183,7 @@ export default {
|
|
|
183
183
|
/** Channel options `{ value, label }`. */
|
|
184
184
|
channelOptions() {
|
|
185
185
|
return Array.isArray(this.content.channels) && this.content.channels.length > 0
|
|
186
|
-
? this.content.channels
|
|
186
|
+
? this.content.channels.map((channel) => ({ ...channel, label: this.copy(channel && channel.label) }))
|
|
187
187
|
: [
|
|
188
188
|
{ value: 'telefoon', label: t('nextcloud-vue', 'Phone') },
|
|
189
189
|
{ value: 'email', label: t('nextcloud-vue', 'Email') },
|
|
@@ -204,10 +204,11 @@ export default {
|
|
|
204
204
|
const prop = properties[this.content.outcomeField || 'outcome']
|
|
205
205
|
if (prop && Array.isArray(prop.enum) && prop.enum.length > 0) {
|
|
206
206
|
const labels = prop['x-enum-labels'] || prop.enumLabels || {}
|
|
207
|
-
|
|
208
|
-
return prop.enum.map((value) => ({ value, label: tr(labels[value] || String(value)) }))
|
|
207
|
+
return prop.enum.map((value) => ({ value, label: this.copy(labels[value] || String(value)) }))
|
|
209
208
|
}
|
|
210
|
-
return Array.isArray(this.content.outcomes)
|
|
209
|
+
return Array.isArray(this.content.outcomes)
|
|
210
|
+
? this.content.outcomes.map((outcome) => ({ ...outcome, label: this.copy(outcome && outcome.label) }))
|
|
211
|
+
: []
|
|
211
212
|
},
|
|
212
213
|
|
|
213
214
|
/**
|
|
@@ -280,8 +281,7 @@ export default {
|
|
|
280
281
|
// `content.submitLabel` names what the form creates, e.g. "Save contact moment".
|
|
281
282
|
registerLabel() {
|
|
282
283
|
if (this.content.submitLabel) {
|
|
283
|
-
|
|
284
|
-
return tr(this.content.submitLabel)
|
|
284
|
+
return this.copy(this.content.submitLabel)
|
|
285
285
|
}
|
|
286
286
|
return t('nextcloud-vue', 'Register')
|
|
287
287
|
},
|
|
@@ -359,6 +359,29 @@ export default {
|
|
|
359
359
|
},
|
|
360
360
|
|
|
361
361
|
/** The default channel value (first configured channel, else `telefoon`). */
|
|
362
|
+
/**
|
|
363
|
+
* A label the manifest supplies (a channel, an outcome, the submit
|
|
364
|
+
* label), translated: the app's own translation first, since the words
|
|
365
|
+
* are the app's, then the library's catalogue, then the text as
|
|
366
|
+
* written. Before, a channel label was shown as written and the submit
|
|
367
|
+
* label only reached the app's translate, so pipelinq showed "Phone"
|
|
368
|
+
* and "Save contact moment" in English to a Dutch reader.
|
|
369
|
+
*
|
|
370
|
+
* @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
|
|
371
|
+
* @param {string} key The label as the manifest writes it.
|
|
372
|
+
* @return {string} The translated label.
|
|
373
|
+
*/
|
|
374
|
+
copy(key) {
|
|
375
|
+
if (typeof key !== 'string' || key === '') {
|
|
376
|
+
return key
|
|
377
|
+
}
|
|
378
|
+
const host = typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : key
|
|
379
|
+
if (typeof host === 'string' && host !== '' && host !== key) {
|
|
380
|
+
return host
|
|
381
|
+
}
|
|
382
|
+
return t('nextcloud-vue', key)
|
|
383
|
+
},
|
|
384
|
+
|
|
362
385
|
firstChannel() {
|
|
363
386
|
const ch = (this.content && this.content.channels) || []
|
|
364
387
|
return (Array.isArray(ch) && ch.length > 0 && ch[0].value) || 'telefoon'
|
|
@@ -121,7 +121,7 @@ export default {
|
|
|
121
121
|
| `icon` | Object\|Function | `null` | MDI icon component shown in the card header |
|
|
122
122
|
| `objectType` | String | `''` | Registered object type slug used by `objectStore.saveObject()` |
|
|
123
123
|
| `store` | Object | `null` | Explicit Pinia objectStore instance; auto-detected via Pinia when omitted |
|
|
124
|
-
| `overrides` | Object | `{}` | Per-property config: `{ key: { order, gridColumn, gridRow, hidden, editable, label, widget } }` |
|
|
124
|
+
| `overrides` | Object | `{}` | Per-property config: `{ key: { order, gridColumn, gridRow, hidden, editable, label, widget, visibleWhen } }`. `visibleWhen` shows the field only when the shared visibility predicate holds for this object, in its local mode: `{ field, op, value }` (ops `eq`, `neq`, `in`, `empty`, ...), or `all` / `any` of those. It reads unsaved changes too. A rule that needs a request (`endpoint`, `source`) is ignored. Read grid only: the Edit form still offers the field. |
|
|
125
125
|
| `columns` | Number | `3` | Number of CSS grid columns in the widget |
|
|
126
126
|
| `collapsedFields` (`collapsed-fields`) | Number | `null` | Number of fields shown while collapsed, with a "Show all N fields" button for the rest. `null` fits the cell: the whole rows that fit stay, and the button only appears when the cell overflows |
|
|
127
127
|
| `hideEmpty` | Boolean | `false` | Hide fields with no value instead of rendering an em dash. For discriminated supertypes (one `ticket` schema holding request/complaint/contactmoment), so the grid is type-aware without the schema enumerating which fields belong to which variant. Read grid only — the field being edited, a field with unsaved changes, and the Edit form stay visible; `false`/`0` are values and are never hidden. |
|
|
@@ -418,6 +418,7 @@ import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
|
418
418
|
import { fieldsFromSchema, formatValue } from '../../utils/schema.js'
|
|
419
419
|
import { schemaRefSlug } from '../../utils/schemaRefSlug.js'
|
|
420
420
|
import { resolveNextcloudUser, searchNextcloudUsers } from '../../utils/userAutocomplete.js'
|
|
421
|
+
import { evaluateVisibleWhenLocal, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'
|
|
421
422
|
import { CnIcon } from '../CnIcon/index.js'
|
|
422
423
|
import { CnObjectMetadataModal } from '../CnObjectMetadataModal/index.js'
|
|
423
424
|
import { CnWidgetWrapper } from '../CnWidgetWrapper/index.js'
|
|
@@ -939,13 +940,18 @@ export default {
|
|
|
939
940
|
gridRow: (this.resolvedOverrides[field.key] && this.resolvedOverrides[field.key].gridRow) || 1,
|
|
940
941
|
}))
|
|
941
942
|
|
|
943
|
+
// Per-field condition: `overrides[key].visibleWhen` shows a field only
|
|
944
|
+
// for the objects it belongs to, such as a person's preferred name on
|
|
945
|
+
// a client that can also be an organisation.
|
|
946
|
+
const shown = withSpans.filter((field) => this.isVisibleByCondition(field))
|
|
947
|
+
|
|
942
948
|
if (!this.hideEmpty) {
|
|
943
|
-
return
|
|
949
|
+
return shown
|
|
944
950
|
}
|
|
945
951
|
|
|
946
952
|
// Keep a field the user is currently editing or has pending changes for,
|
|
947
953
|
// otherwise it would vanish mid-edit the moment its value is cleared.
|
|
948
|
-
return
|
|
954
|
+
return shown.filter((field) => field.key === this.editingField
|
|
949
955
|
|| field.key in this.dirtyFields
|
|
950
956
|
|| !this.isEmptyValue((this.objectData || {})[field.key]))
|
|
951
957
|
},
|
|
@@ -1709,6 +1715,29 @@ export default {
|
|
|
1709
1715
|
return !field.readOnly
|
|
1710
1716
|
},
|
|
1711
1717
|
|
|
1718
|
+
/**
|
|
1719
|
+
* Evaluate a field's `visibleWhen` override against the object.
|
|
1720
|
+
*
|
|
1721
|
+
* The rule is the shared visibility predicate in its LOCAL mode
|
|
1722
|
+
* (`{ field, op, value }`, `all` / `any`, `appInstalled`), evaluated by
|
|
1723
|
+
* `evaluateVisibleWhenLocal` against the object with its unsaved
|
|
1724
|
+
* changes, so a field appears or goes the moment the field it depends
|
|
1725
|
+
* on changes. A rule that needs a request (`endpoint`, `source`) cannot
|
|
1726
|
+
* be decided here and is ignored: the field shows, as it did before
|
|
1727
|
+
* the rule existed.
|
|
1728
|
+
*
|
|
1729
|
+
* @param {object} field - Resolved field definition.
|
|
1730
|
+
* @return {boolean} True when the field is shown.
|
|
1731
|
+
*/
|
|
1732
|
+
isVisibleByCondition(field) {
|
|
1733
|
+
const override = this.resolvedOverrides[field.key]
|
|
1734
|
+
const rule = override ? override.visibleWhen : undefined
|
|
1735
|
+
if (rule === undefined || rule === null || !isLocallyDecidableVisibleWhen(rule)) {
|
|
1736
|
+
return true
|
|
1737
|
+
}
|
|
1738
|
+
return evaluateVisibleWhenLocal(rule, { ...(this.objectData || {}), ...this.dirtyFields })
|
|
1739
|
+
},
|
|
1740
|
+
|
|
1712
1741
|
/**
|
|
1713
1742
|
* Evaluate a field's conditional read-only rule against the object data.
|
|
1714
1743
|
*
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
-->
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
|
-
<fieldset class="cn-radio-cards"
|
|
7
|
+
<fieldset class="cn-radio-cards"
|
|
8
|
+
:class="{ 'cn-radio-cards--list': isList }"
|
|
9
|
+
data-testid="cn-radio-cards"
|
|
10
|
+
:aria-busy="busy ? 'true' : 'false'">
|
|
8
11
|
<legend v-if="label" class="cn-radio-cards__legend">
|
|
9
12
|
{{ label }}
|
|
10
13
|
</legend>
|
|
@@ -52,7 +55,7 @@ let uid = 0
|
|
|
52
55
|
* underneath, so the choice is perceivable without colour and works from the
|
|
53
56
|
* keyboard.
|
|
54
57
|
*
|
|
55
|
-
* @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md
|
|
58
|
+
* @spec openspec/changes/abstract-radio-cards/specs/abstract-radio-cards/spec.md
|
|
56
59
|
*/
|
|
57
60
|
export default {
|
|
58
61
|
name: 'CnRadioCardsWidget',
|
|
@@ -72,6 +75,7 @@ export default {
|
|
|
72
75
|
* - `options` (required) — `[{ value, label, description? }]`.
|
|
73
76
|
* - `label` — a legend over the cards (visually shown).
|
|
74
77
|
* - `columns` — 1, 2 or 3 (default 2).
|
|
78
|
+
* - `variant` — `cards` (the default) or `list`: a compact list of radio buttons, one per line, with the description under the label and no card around each (board DcBeheer). A list is one column.
|
|
75
79
|
* - `readOnly`.
|
|
76
80
|
*
|
|
77
81
|
* @type {object}
|
|
@@ -133,7 +137,20 @@ export default {
|
|
|
133
137
|
return this.tr(this.content.label || '')
|
|
134
138
|
},
|
|
135
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Whether the options draw as a compact list (`variant: "list"`).
|
|
142
|
+
*
|
|
143
|
+
* @return {boolean}
|
|
144
|
+
* @spec openspec/changes/screens-radio-cards-list/specs/radio-cards/spec.md#requirement-radio-cards-can-draw-as-a-compact-list
|
|
145
|
+
*/
|
|
146
|
+
isList() {
|
|
147
|
+
return this.content.variant === 'list'
|
|
148
|
+
},
|
|
149
|
+
|
|
136
150
|
columns() {
|
|
151
|
+
if (this.isList) {
|
|
152
|
+
return 1
|
|
153
|
+
}
|
|
137
154
|
return [1, 3].includes(this.content.columns) ? this.content.columns : 2
|
|
138
155
|
},
|
|
139
156
|
|
|
@@ -185,7 +202,7 @@ export default {
|
|
|
185
202
|
*
|
|
186
203
|
* @param {object} option The chosen option.
|
|
187
204
|
* @return {Promise<void>}
|
|
188
|
-
* @spec openspec/changes/abstract-radio-cards/specs/radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field
|
|
205
|
+
* @spec openspec/changes/abstract-radio-cards/specs/abstract-radio-cards/spec.md#requirement-choosing-a-card-saves-that-one-field
|
|
189
206
|
*/
|
|
190
207
|
async choose(option) {
|
|
191
208
|
if ((!this.objectUrl && !this.hasSave) || !this.content.field) {
|
|
@@ -310,6 +327,40 @@ export default {
|
|
|
310
327
|
color: var(--color-text-maxcontrast);
|
|
311
328
|
}
|
|
312
329
|
|
|
330
|
+
/* `variant: "list"`: one radio button per line, the description under the
|
|
331
|
+
label, no card around each (board DcBeheer, "Soort organisatie"). */
|
|
332
|
+
.cn-radio-cards--list .cn-radio-cards__grid {
|
|
333
|
+
gap: 8px;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.cn-radio-cards--list .cn-radio-cards__card,
|
|
337
|
+
.cn-radio-cards--list .cn-radio-cards__card--selected {
|
|
338
|
+
padding: 0;
|
|
339
|
+
border: 0;
|
|
340
|
+
border-radius: 0;
|
|
341
|
+
background: transparent;
|
|
342
|
+
box-shadow: none;
|
|
343
|
+
font-size: 15px;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.cn-radio-cards--list .cn-radio-cards__dot {
|
|
347
|
+
width: 18px;
|
|
348
|
+
height: 18px;
|
|
349
|
+
margin-top: 2px;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.cn-radio-cards--list .cn-radio-cards__card--selected .cn-radio-cards__dot {
|
|
353
|
+
border-width: 5px;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.cn-radio-cards--list .cn-radio-cards__title {
|
|
357
|
+
font-weight: 400;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.cn-radio-cards--list .cn-radio-cards__description {
|
|
361
|
+
font-size: 13px;
|
|
362
|
+
}
|
|
363
|
+
|
|
313
364
|
@media (max-width: 640px) {
|
|
314
365
|
.cn-radio-cards__grid--2,
|
|
315
366
|
.cn-radio-cards__grid--3 {
|
|
@@ -212,7 +212,9 @@ export default {
|
|
|
212
212
|
/**
|
|
213
213
|
* The list config: `{ register, schema, filter?, sort?, limit?, extend?,
|
|
214
214
|
* presentation?, title?, meta?, text?, label?, pill?, trailing?, icon?,
|
|
215
|
-
* rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText
|
|
215
|
+
* rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText?,
|
|
216
|
+
* valueLabels? }`. `valueLabels` maps a field's stored values to the labels
|
|
217
|
+
* the templates show.
|
|
216
218
|
*
|
|
217
219
|
* @type {object}
|
|
218
220
|
*/
|
|
@@ -441,7 +443,40 @@ export default {
|
|
|
441
443
|
* @return {string} The text, '' when a token is empty.
|
|
442
444
|
*/
|
|
443
445
|
fill(template, row) {
|
|
444
|
-
return typeof template === 'string' && template !== '' ? fillTemplate(this.translate(template), row, new Date(), this.templateContext) : ''
|
|
446
|
+
return typeof template === 'string' && template !== '' ? fillTemplate(this.translate(template), this.labelledRow(row), new Date(), this.templateContext) : ''
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* The row as the templates read it: a field named in `valueLabels`
|
|
451
|
+
* (`{ channel: { phone: "Phone", telefoon: "Phone" } }`) shows the
|
|
452
|
+
* translated label of its stored value, so "{channel}" reads "Telefoon"
|
|
453
|
+
* for both spellings the data holds. A value without a label stays as
|
|
454
|
+
* stored. The pill, the badges and the sort read the stored value.
|
|
455
|
+
*
|
|
456
|
+
* @param {object} row The row.
|
|
457
|
+
* @return {object} The row, or a shallow copy with labelled fields.
|
|
458
|
+
* @spec openspec/changes/screens-record-list-value-labels/specs/record-list-value-labels/spec.md#requirement-a-record-list-template-can-show-a-label-for-a-stored-value
|
|
459
|
+
*/
|
|
460
|
+
labelledRow(row) {
|
|
461
|
+
const map = this.content?.valueLabels
|
|
462
|
+
if (!row || typeof row !== 'object' || !map || typeof map !== 'object') {
|
|
463
|
+
return row
|
|
464
|
+
}
|
|
465
|
+
let out = row
|
|
466
|
+
for (const [field, labels] of Object.entries(map)) {
|
|
467
|
+
const raw = row[field]
|
|
468
|
+
if (!labels || typeof labels !== 'object' || (typeof raw !== 'string' && typeof raw !== 'number')) {
|
|
469
|
+
continue
|
|
470
|
+
}
|
|
471
|
+
const label = labels[String(raw)]
|
|
472
|
+
if (typeof label === 'string' && label !== '') {
|
|
473
|
+
if (out === row) {
|
|
474
|
+
out = { ...row }
|
|
475
|
+
}
|
|
476
|
+
out[field] = this.translate(label)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return out
|
|
445
480
|
},
|
|
446
481
|
|
|
447
482
|
/**
|
|
@@ -32,9 +32,33 @@
|
|
|
32
32
|
Spec: hydra openspec/architecture/adr-112-reports-are-one-page.md (ConductionNL/hydra#640).
|
|
33
33
|
-->
|
|
34
34
|
<template>
|
|
35
|
-
<div class="cn-reports-page">
|
|
35
|
+
<div class="cn-reports-page" :class="{ 'cn-reports-page--board': isBoard }">
|
|
36
36
|
<div class="cn-reports-page__header">
|
|
37
|
-
|
|
37
|
+
<!-- The header buttons (`headerButtons`) beside the title, as the
|
|
38
|
+
board's "Downloaden" and the buildiq square (board DcRapportages).
|
|
39
|
+
Without them, and outside the board look, the title stands alone
|
|
40
|
+
as before. -->
|
|
41
|
+
<div v-if="headerButtonsRow.length > 0" class="cn-reports-page__heading-row">
|
|
42
|
+
<h2 class="cn-reports-page__title">
|
|
43
|
+
{{ resolvedTitle }}
|
|
44
|
+
</h2>
|
|
45
|
+
<div class="cn-reports-page__header-buttons" data-testid="cn-reports-header-buttons">
|
|
46
|
+
<template v-for="button in headerButtonsRow" :key="button.key">
|
|
47
|
+
<CnBuildiqEditButton v-if="button.action === '__buildiq'" />
|
|
48
|
+
<NcButton
|
|
49
|
+
v-else
|
|
50
|
+
:variant="button.variant"
|
|
51
|
+
:data-testid="`cn-reports-header-button-${button.key}`"
|
|
52
|
+
@click="onHeaderButton(button)">
|
|
53
|
+
<template v-if="button.icon" #icon>
|
|
54
|
+
<CnIcon :name="button.icon" :size="20" />
|
|
55
|
+
</template>
|
|
56
|
+
{{ button.label }}
|
|
57
|
+
</NcButton>
|
|
58
|
+
</template>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<h2 v-else class="cn-reports-page__title">
|
|
38
62
|
{{ resolvedTitle }}
|
|
39
63
|
</h2>
|
|
40
64
|
<p v-if="resolvedDescription" class="cn-reports-page__description">
|
|
@@ -42,7 +66,44 @@
|
|
|
42
66
|
</p>
|
|
43
67
|
</div>
|
|
44
68
|
|
|
45
|
-
|
|
69
|
+
<!-- The board look (board DcRapportages): one section per category,
|
|
70
|
+
headed by its label, instead of the category filter; each card
|
|
71
|
+
draws its icon in a tile. -->
|
|
72
|
+
<template v-if="isBoard">
|
|
73
|
+
<p v-if="resolvedCards.length === 0"
|
|
74
|
+
class="cn-reports-page__empty"
|
|
75
|
+
data-testid="cn-reports-empty">
|
|
76
|
+
{{ resolvedEmptyLabel }}
|
|
77
|
+
</p>
|
|
78
|
+
<section v-for="section in boardSections"
|
|
79
|
+
:key="section.key"
|
|
80
|
+
class="cn-reports-page__section"
|
|
81
|
+
data-testid="cn-reports-section">
|
|
82
|
+
<h3 v-if="section.label" class="cn-reports-page__section-title">
|
|
83
|
+
{{ section.label }}
|
|
84
|
+
</h3>
|
|
85
|
+
<ul class="cn-reports-page__grid" data-testid="cn-reports-grid">
|
|
86
|
+
<li v-for="card in section.cards" :key="card.id" class="cn-reports-page__cell">
|
|
87
|
+
<a class="cn-reports-page__card"
|
|
88
|
+
data-testid="cn-report-card"
|
|
89
|
+
:href="hrefFor(card)"
|
|
90
|
+
@click="open(card, $event)">
|
|
91
|
+
<span v-if="card.icon" class="cn-reports-page__card-icon" aria-hidden="true">
|
|
92
|
+
<CnIcon :name="card.icon" :size="22" />
|
|
93
|
+
</span>
|
|
94
|
+
<span class="cn-reports-page__card-text">
|
|
95
|
+
<span class="cn-reports-page__card-title">{{ card.label }}</span>
|
|
96
|
+
<span v-if="card.description" class="cn-reports-page__card-description">
|
|
97
|
+
{{ card.description }}
|
|
98
|
+
</span>
|
|
99
|
+
</span>
|
|
100
|
+
</a>
|
|
101
|
+
</li>
|
|
102
|
+
</ul>
|
|
103
|
+
</section>
|
|
104
|
+
</template>
|
|
105
|
+
|
|
106
|
+
<div v-if="!isBoard && categoryOptions.length > 1" class="cn-reports-page__filter">
|
|
46
107
|
<label class="cn-reports-page__filter-label" :for="filterId">
|
|
47
108
|
{{ resolvedCategoryLabel }}
|
|
48
109
|
</label>
|
|
@@ -58,13 +119,13 @@
|
|
|
58
119
|
</select>
|
|
59
120
|
</div>
|
|
60
121
|
|
|
61
|
-
<p v-if="visibleCards.length === 0"
|
|
122
|
+
<p v-if="!isBoard && visibleCards.length === 0"
|
|
62
123
|
class="cn-reports-page__empty"
|
|
63
124
|
data-testid="cn-reports-empty">
|
|
64
125
|
{{ resolvedEmptyLabel }}
|
|
65
126
|
</p>
|
|
66
127
|
|
|
67
|
-
<ul v-else class="cn-reports-page__grid" data-testid="cn-reports-grid">
|
|
128
|
+
<ul v-else-if="!isBoard" class="cn-reports-page__grid" data-testid="cn-reports-grid">
|
|
68
129
|
<li v-for="card in visibleCards" :key="card.id" class="cn-reports-page__cell">
|
|
69
130
|
<a class="cn-reports-page__card"
|
|
70
131
|
data-testid="cn-report-card"
|
|
@@ -97,7 +158,12 @@
|
|
|
97
158
|
* The Reports page: one surface listing an app's reports as cards.
|
|
98
159
|
*/
|
|
99
160
|
import { translate as t } from '@nextcloud/l10n'
|
|
161
|
+
import { NcButton } from '@nextcloud/vue'
|
|
100
162
|
import { defineAsyncComponent } from 'vue'
|
|
163
|
+
import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
|
|
164
|
+
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
165
|
+
import { useLook } from '../../composables/useLook.js'
|
|
166
|
+
import { resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'
|
|
101
167
|
import { followLinkClick } from '../../utils/linkNavigation.js'
|
|
102
168
|
|
|
103
169
|
export default {
|
|
@@ -107,9 +173,18 @@ export default {
|
|
|
107
173
|
// Loaded only for a page that declares widgets: a cards-only reports
|
|
108
174
|
// page does not pull the dashboard in.
|
|
109
175
|
CnDashboardPage: defineAsyncComponent(() => import('../CnDashboardPage/CnDashboardPage.vue').then((m) => m.default)),
|
|
176
|
+
CnBuildiqEditButton,
|
|
177
|
+
CnIcon,
|
|
178
|
+
NcButton,
|
|
110
179
|
},
|
|
111
180
|
|
|
112
181
|
inject: {
|
|
182
|
+
/**
|
|
183
|
+
* The v2 registry and the legacy customComponents map, provided by
|
|
184
|
+
* CnAppRoot: a header action's named handler resolves against them.
|
|
185
|
+
*/
|
|
186
|
+
cnRegistry: { default: () => ({}) },
|
|
187
|
+
cnCustomComponents: { default: () => ({}) },
|
|
113
188
|
/**
|
|
114
189
|
* The consuming app's translate function, provided by CnAppRoot.
|
|
115
190
|
* Defaults to identity so the page still renders outside one.
|
|
@@ -228,6 +303,49 @@ export default {
|
|
|
228
303
|
default: null,
|
|
229
304
|
},
|
|
230
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Buttons beside the title (`config.headerButtons`), as the board's
|
|
308
|
+
* "Downloaden". Each is `{ label, action, variant?, icon?, id? }`:
|
|
309
|
+
* `action` is the id of a `headerActions` entry, which says what the
|
|
310
|
+
* click does. Under the board look the buildiq square sits between the
|
|
311
|
+
* secondary buttons and the primary one. Empty (the default) changes
|
|
312
|
+
* nothing.
|
|
313
|
+
*
|
|
314
|
+
* @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
|
|
315
|
+
* @type {Array<{label: string, action: string, variant?: ('primary'|'secondary'), icon?: string, id?: string}>|null}
|
|
316
|
+
*/
|
|
317
|
+
headerButtons: {
|
|
318
|
+
type: Array,
|
|
319
|
+
default: null,
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The page's header actions (`config.headerActions`), in the shape of
|
|
324
|
+
* an index page's: a header button names one by id. `handler` is a
|
|
325
|
+
* function, `navigate` (with `route`, and `params`), `emit`, `none`,
|
|
326
|
+
* or a registry name; `type: "navigate"` with a `target` URL opens
|
|
327
|
+
* that URL in a new tab. Every click also emits `header-action`
|
|
328
|
+
* `{ action, id }`, except for `handler: "none"`.
|
|
329
|
+
*
|
|
330
|
+
* @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
|
|
331
|
+
* @type {Array<object>|null}
|
|
332
|
+
*/
|
|
333
|
+
headerActions: {
|
|
334
|
+
type: Array,
|
|
335
|
+
default: null,
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The look to draw in: `board` or `nextcloud`. Absent: the look
|
|
340
|
+
* CnAppRoot provides (`cnLook`), else `nextcloud`.
|
|
341
|
+
*
|
|
342
|
+
* @type {string|null}
|
|
343
|
+
*/
|
|
344
|
+
look: {
|
|
345
|
+
type: String,
|
|
346
|
+
default: null,
|
|
347
|
+
},
|
|
348
|
+
|
|
231
349
|
/**
|
|
232
350
|
* Translate function. Falls back to injected `cnTranslate`.
|
|
233
351
|
*
|
|
@@ -239,6 +357,13 @@ export default {
|
|
|
239
357
|
},
|
|
240
358
|
},
|
|
241
359
|
|
|
360
|
+
emits: ['header-action'],
|
|
361
|
+
|
|
362
|
+
setup(props) {
|
|
363
|
+
const { isBoard } = useLook(props)
|
|
364
|
+
return { isBoard }
|
|
365
|
+
},
|
|
366
|
+
|
|
242
367
|
data() {
|
|
243
368
|
return {
|
|
244
369
|
activeCategory: 'all',
|
|
@@ -283,6 +408,8 @@ export default {
|
|
|
283
408
|
'emptyLabel',
|
|
284
409
|
'widgets',
|
|
285
410
|
'layout',
|
|
411
|
+
'headerButtons',
|
|
412
|
+
'headerActions',
|
|
286
413
|
]) {
|
|
287
414
|
if (this[key] !== null && this[key] !== undefined) {
|
|
288
415
|
fromProps[key] = this[key]
|
|
@@ -322,6 +449,51 @@ export default {
|
|
|
322
449
|
})
|
|
323
450
|
},
|
|
324
451
|
|
|
452
|
+
/**
|
|
453
|
+
* The declared header buttons that have a label and an action,
|
|
454
|
+
* translated, each with a stable key and a variant.
|
|
455
|
+
*
|
|
456
|
+
* @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
|
|
457
|
+
* @return {Array<{key: string, label: string, action: string, variant: string, icon: string}>} The buttons.
|
|
458
|
+
*/
|
|
459
|
+
resolvedHeaderButtons() {
|
|
460
|
+
const declared = Array.isArray(this.config.headerButtons) ? this.config.headerButtons : []
|
|
461
|
+
return declared
|
|
462
|
+
.filter((button) => button && typeof button === 'object'
|
|
463
|
+
&& typeof button.action === 'string' && button.action !== ''
|
|
464
|
+
&& typeof button.label === 'string' && button.label !== '')
|
|
465
|
+
.map((button) => ({
|
|
466
|
+
key: String(button.id || button.action),
|
|
467
|
+
label: this.tr(button.label),
|
|
468
|
+
action: button.action,
|
|
469
|
+
variant: button.variant === 'primary' ? 'primary' : 'secondary',
|
|
470
|
+
icon: typeof button.icon === 'string' ? button.icon : '',
|
|
471
|
+
}))
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The header row in the order it renders. Without the board look:
|
|
476
|
+
* the declared buttons in the declared order. Under the board look:
|
|
477
|
+
* the secondary buttons, the buildiq square, then the primary ones,
|
|
478
|
+
* as the board draws "Downloaden" and the square. No declared button
|
|
479
|
+
* leaves the header as it was, without the square.
|
|
480
|
+
*
|
|
481
|
+
* @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
|
|
482
|
+
* @return {Array<object>} The buttons; the square is `{ key: '__buildiq', action: '__buildiq' }`.
|
|
483
|
+
*/
|
|
484
|
+
headerButtonsRow() {
|
|
485
|
+
const buttons = this.resolvedHeaderButtons
|
|
486
|
+
if (buttons.length === 0 || !this.isBoard) {
|
|
487
|
+
return buttons
|
|
488
|
+
}
|
|
489
|
+
const square = { key: '__buildiq', action: '__buildiq', variant: 'secondary', label: '', icon: '' }
|
|
490
|
+
return [
|
|
491
|
+
...buttons.filter((button) => button.variant !== 'primary'),
|
|
492
|
+
square,
|
|
493
|
+
...buttons.filter((button) => button.variant === 'primary'),
|
|
494
|
+
]
|
|
495
|
+
},
|
|
496
|
+
|
|
325
497
|
/**
|
|
326
498
|
* The heading.
|
|
327
499
|
*
|
|
@@ -385,6 +557,30 @@ export default {
|
|
|
385
557
|
return options
|
|
386
558
|
},
|
|
387
559
|
|
|
560
|
+
/**
|
|
561
|
+
* The board look's sections: the cards without a category first, under
|
|
562
|
+
* no heading, then one section per declared category that holds a
|
|
563
|
+
* card, in the declared order, headed by its label.
|
|
564
|
+
*
|
|
565
|
+
* @spec openspec/changes/screens-reports-page-board-look/specs/reports-page/spec.md#requirement-the-reports-page-takes-the-board-look
|
|
566
|
+
* @return {Array<{key: string, label: string, cards: Array<object>}>} The sections.
|
|
567
|
+
*/
|
|
568
|
+
boardSections() {
|
|
569
|
+
const declared = this.config.categories || {}
|
|
570
|
+
const sections = []
|
|
571
|
+
const loose = this.resolvedCards.filter((card) => !card.category || !Object.hasOwn(declared, card.category))
|
|
572
|
+
if (loose.length > 0) {
|
|
573
|
+
sections.push({ key: '__none', label: '', cards: loose })
|
|
574
|
+
}
|
|
575
|
+
for (const key of Object.keys(declared)) {
|
|
576
|
+
const cards = this.resolvedCards.filter((card) => card.category === key)
|
|
577
|
+
if (cards.length > 0) {
|
|
578
|
+
sections.push({ key, label: this.tr(declared[key]), cards })
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return sections
|
|
582
|
+
},
|
|
583
|
+
|
|
388
584
|
/**
|
|
389
585
|
* The cards the active filter admits.
|
|
390
586
|
*
|
|
@@ -487,6 +683,47 @@ export default {
|
|
|
487
683
|
}
|
|
488
684
|
},
|
|
489
685
|
|
|
686
|
+
/**
|
|
687
|
+
* A header button was clicked: run the `headerActions` entry it names
|
|
688
|
+
* and emit `header-action`, as an index page's header button does.
|
|
689
|
+
*
|
|
690
|
+
* @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
|
|
691
|
+
* @param {{action: string}} button The resolved button.
|
|
692
|
+
* @return {void}
|
|
693
|
+
*/
|
|
694
|
+
onHeaderButton(button) {
|
|
695
|
+
const id = button.action
|
|
696
|
+
const actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : []
|
|
697
|
+
const entry = actions.find((action) => action && action.id === id) || null
|
|
698
|
+
const handler = entry ? entry.handler : undefined
|
|
699
|
+
if (handler === 'none') {
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
if (typeof handler === 'function') {
|
|
703
|
+
handler({ actionId: id })
|
|
704
|
+
} else if (handler === 'navigate' && entry.route) {
|
|
705
|
+
const params = (entry.params && typeof entry.params === 'object') ? entry.params : null
|
|
706
|
+
if (this.$router && typeof this.$router.push === 'function') {
|
|
707
|
+
this.$router.push(params ? { name: entry.route, params } : { name: entry.route })
|
|
708
|
+
}
|
|
709
|
+
} else if (entry && entry.type === 'navigate' && typeof entry.target === 'string' && entry.target !== '') {
|
|
710
|
+
window.open(entry.target, '_blank', 'noopener,noreferrer')
|
|
711
|
+
} else if (typeof handler === 'string' && handler !== 'emit') {
|
|
712
|
+
const resolved = resolveRegisteredHandler(handler, this.cnRegistry, this.cnCustomComponents)
|
|
713
|
+
if (typeof resolved === 'function') {
|
|
714
|
+
resolved({ actionId: id })
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* A header button was clicked (`headerButtons`), after its
|
|
719
|
+
* `headerActions` entry ran; not for `handler: "none"`.
|
|
720
|
+
*
|
|
721
|
+
* @event header-action
|
|
722
|
+
* @property {{action: string, id: string}} payload The id of the `headerActions` entry the button names, as `action` and `id`.
|
|
723
|
+
*/
|
|
724
|
+
this.$emit('header-action', { action: id, id })
|
|
725
|
+
},
|
|
726
|
+
|
|
490
727
|
/**
|
|
491
728
|
* Navigate by route NAME on a plain click; a modified or middle click
|
|
492
729
|
* is left to the browser so it can open the card's href elsewhere.
|
|
@@ -518,6 +755,27 @@ export default {
|
|
|
518
755
|
margin: 0 0 4px;
|
|
519
756
|
}
|
|
520
757
|
|
|
758
|
+
.cn-reports-page__heading-row {
|
|
759
|
+
display: flex;
|
|
760
|
+
flex-wrap: wrap;
|
|
761
|
+
align-items: flex-start;
|
|
762
|
+
justify-content: space-between;
|
|
763
|
+
gap: 12px 24px;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.cn-reports-page__heading-row .cn-reports-page__title {
|
|
767
|
+
flex: 1 1 320px;
|
|
768
|
+
min-width: 0;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.cn-reports-page__header-buttons {
|
|
772
|
+
display: flex;
|
|
773
|
+
flex: none;
|
|
774
|
+
flex-wrap: wrap;
|
|
775
|
+
align-items: center;
|
|
776
|
+
gap: 10px;
|
|
777
|
+
}
|
|
778
|
+
|
|
521
779
|
.cn-reports-page__description {
|
|
522
780
|
margin: 0 0 16px;
|
|
523
781
|
color: var(--color-text-maxcontrast);
|