@conduction/nextcloud-vue 2.83.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/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 +5 -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/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/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/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 +135 -2
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
- package/dist/esm/components/CnReportsPage/CnReportsPage.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 +1 -1
- 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 +17 -2
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.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 +38 -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 +448 -296
- 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/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/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +1706 -454
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +448 -296
- package/l10n/en.json +16 -1
- package/l10n/nl.json +38 -1
- package/package.json +1 -1
- 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 +5 -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/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +31 -2
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
- package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
- package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
- package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
- 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/look-board-reports.css +58 -0
- package/src/css/look-board.css +12 -0
- package/src/dialogs/CnTransitionInputDialog.vue +87 -7
- package/src/schemas/app-manifest-v2.schema.json +23 -2
- package/src/utils/buildManifest.js +36 -2
- package/src/utils/detailWidgetVisibility.js +145 -0
- package/src/utils/formatMetric.js +38 -2
- package/src/utils/resolveManifestTokens.js +1 -0
- package/src/utils/sentinelTokens.js +9 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -1253,6 +1253,7 @@ import {
|
|
|
1253
1253
|
stageEntry,
|
|
1254
1254
|
stageOf,
|
|
1255
1255
|
} from '../../utils/detailActionModel.js'
|
|
1256
|
+
import { detailWidgetVisible, fetchVisibilityRows, visibilityContextRefs, widgetConditionData } from '../../utils/detailWidgetVisibility.js'
|
|
1256
1257
|
import { reportBindingProblems } from '../../utils/diagnostics.js'
|
|
1257
1258
|
import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
|
|
1258
1259
|
import { cnGridCellStyle, hasGridRow } from '../../utils/grid.js'
|
|
@@ -1260,6 +1261,7 @@ import { isActivityWidget, resolveHeaderMeta } from '../../utils/headerMeta.js'
|
|
|
1260
1261
|
import { patchStoredSelf } from '../../utils/patchStoredSelf.js'
|
|
1261
1262
|
import { setReadState } from '../../utils/recordInteractions.js'
|
|
1262
1263
|
import { slotRenders } from '../../utils/slotContent.js'
|
|
1264
|
+
import { evaluateVisibleWhen, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'
|
|
1263
1265
|
import {
|
|
1264
1266
|
isCardWidgetDef,
|
|
1265
1267
|
isContentOnlyWidgetDef,
|
|
@@ -1512,8 +1514,8 @@ export default {
|
|
|
1512
1514
|
|
|
1513
1515
|
/**
|
|
1514
1516
|
* Offer the board look's tab list name to the tabs widget below this page
|
|
1515
|
-
* (`config.tabsLabel`, default "<type> parts")
|
|
1516
|
-
* widget reads
|
|
1517
|
+
* (`config.tabsLabel`, default "<type> parts"), and the records
|
|
1518
|
+
* `visibilityContext` names. Getter objects, so the widget reads them live.
|
|
1517
1519
|
*
|
|
1518
1520
|
* @return {object} The provided values.
|
|
1519
1521
|
* @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-folder-tabs-take-the-board-strip
|
|
@@ -1521,6 +1523,7 @@ export default {
|
|
|
1521
1523
|
provide() {
|
|
1522
1524
|
const isBoard = computed(() => this.isBoard)
|
|
1523
1525
|
const tabsLabel = computed(() => this.resolvedTabsLabel)
|
|
1526
|
+
const visibilityRows = computed(() => (this.visibilityRowsPending ? null : this.visibilityRows))
|
|
1524
1527
|
return {
|
|
1525
1528
|
// The page's own look reaches the tabs, the cards and the activity
|
|
1526
1529
|
// below it, also when it came from the `look` prop and not the app.
|
|
@@ -1530,6 +1533,15 @@ export default {
|
|
|
1530
1533
|
return tabsLabel.value
|
|
1531
1534
|
},
|
|
1532
1535
|
},
|
|
1536
|
+
|
|
1537
|
+
// The records `visibilityContext` names, keyed `@context.<name>`, so a tab's
|
|
1538
|
+
// `visibleWhen` reads the case type as a grid widget's does. Null
|
|
1539
|
+
// while they are on their way.
|
|
1540
|
+
cnDetailVisibilityRows: {
|
|
1541
|
+
get value() {
|
|
1542
|
+
return visibilityRows.value
|
|
1543
|
+
},
|
|
1544
|
+
},
|
|
1533
1545
|
}
|
|
1534
1546
|
},
|
|
1535
1547
|
|
|
@@ -2572,6 +2584,25 @@ export default {
|
|
|
2572
2584
|
default: '',
|
|
2573
2585
|
},
|
|
2574
2586
|
|
|
2587
|
+
/**
|
|
2588
|
+
* Records the page reads beside the loaded one, so a `visibleWhen`
|
|
2589
|
+
* can ask about them (manifest `config.visibilityContext`). Each entry
|
|
2590
|
+
* is `{ <name>: { field, register, schema } }`: `field` is the
|
|
2591
|
+
* property of the loaded record holding the other record's id, and a
|
|
2592
|
+
* condition reads that record as `@context.<name>.<path>`. A case page that
|
|
2593
|
+
* declares `{ caseType: { field: "caseType", register: "dossiq",
|
|
2594
|
+
* schema: "caseType" } }` can show a widget for some case types only
|
|
2595
|
+
* with `{ field: "@context.caseType.category", op: "in", value: [...] }`.
|
|
2596
|
+
* A record that cannot be read is absent, so its condition fails safe.
|
|
2597
|
+
* Empty (the default) reads nothing extra.
|
|
2598
|
+
*
|
|
2599
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
2600
|
+
*/
|
|
2601
|
+
visibilityContext: {
|
|
2602
|
+
type: Object,
|
|
2603
|
+
default: () => ({}),
|
|
2604
|
+
},
|
|
2605
|
+
|
|
2575
2606
|
/**
|
|
2576
2607
|
* Whether the current user administers this instance. Shows the
|
|
2577
2608
|
* `adminOnly` header actions. `null` (the default) reads it from
|
|
@@ -2799,6 +2830,19 @@ export default {
|
|
|
2799
2830
|
|
|
2800
2831
|
data() {
|
|
2801
2832
|
return {
|
|
2833
|
+
/**
|
|
2834
|
+
* The records `visibilityContext` names, keyed `@context.<name>`: the row,
|
|
2835
|
+
* `null` when it could not be read, absent while it is on its way.
|
|
2836
|
+
*/
|
|
2837
|
+
visibilityRows: {},
|
|
2838
|
+
/** The `visibilityRefsKey` the rows in `visibilityRows` were read for. */
|
|
2839
|
+
visibilityRowsFor: '',
|
|
2840
|
+
/** Sequence of the last `visibilityContext` read, so a late answer for another record is dropped. */
|
|
2841
|
+
visibilityRowsSeq: 0,
|
|
2842
|
+
/** The answers of endpoint and source `visibleWhen` conditions on grid widgets, by widget id. */
|
|
2843
|
+
widgetConditionAnswers: {},
|
|
2844
|
+
/** Sequence of the last remote condition round. */
|
|
2845
|
+
widgetConditionSeq: 0,
|
|
2802
2846
|
/** The counts of the header's count pills (`countFrom`), by pill key (`type`, `status`). */
|
|
2803
2847
|
pillCounts: {},
|
|
2804
2848
|
/** Sequence of the last pill count round, so a late answer does not overwrite a newer one. */
|
|
@@ -3058,11 +3102,17 @@ export default {
|
|
|
3058
3102
|
* because a keyboard user who cannot see the page needs to know what
|
|
3059
3103
|
* they are being offered before they take it.
|
|
3060
3104
|
*
|
|
3105
|
+
* One library string with a placeholder, not "Skip to" from the host's
|
|
3106
|
+
* catalogue glued to the label: no app carried that fragment, so every
|
|
3107
|
+
* Dutch reader got "Skip to In behandeling nemen" (Zuiddrecht live
|
|
3108
|
+
* round 3), and a fragment cannot be put in Dutch word order anyway.
|
|
3109
|
+
*
|
|
3061
3110
|
* @return {string} The link text.
|
|
3062
3111
|
* @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md
|
|
3112
|
+
* @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language
|
|
3063
3113
|
*/
|
|
3064
3114
|
skipLinkLabel() {
|
|
3065
|
-
return
|
|
3115
|
+
return t('nextcloud-vue', 'Skip to {action}', { action: this.effectivePrimaryLabel })
|
|
3066
3116
|
},
|
|
3067
3117
|
|
|
3068
3118
|
// ── The action model (detail-action-model-and-case-surfaces) ────
|
|
@@ -3904,6 +3954,68 @@ export default {
|
|
|
3904
3954
|
return store.objects?.[this.objectType]?.[this.objectId] ?? null
|
|
3905
3955
|
},
|
|
3906
3956
|
|
|
3957
|
+
/**
|
|
3958
|
+
* The records `visibilityContext` names for the loaded record.
|
|
3959
|
+
*
|
|
3960
|
+
* @return {Array<{key: string, register: string, schema: string, id: string}>}
|
|
3961
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
3962
|
+
*/
|
|
3963
|
+
visibilityRefs() {
|
|
3964
|
+
return visibilityContextRefs(this.visibilityContext, this.resolvedObject)
|
|
3965
|
+
},
|
|
3966
|
+
|
|
3967
|
+
/**
|
|
3968
|
+
* Whether a record `visibilityContext` names is still on its way.
|
|
3969
|
+
*
|
|
3970
|
+
* @return {boolean}
|
|
3971
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
3972
|
+
*/
|
|
3973
|
+
visibilityRowsPending() {
|
|
3974
|
+
return this.visibilityRefs.length > 0 && this.visibilityRowsFor !== this.visibilityRefsKey
|
|
3975
|
+
},
|
|
3976
|
+
|
|
3977
|
+
/**
|
|
3978
|
+
* The named references as one key, so another record (or another case
|
|
3979
|
+
* type on the same record) reads them again.
|
|
3980
|
+
*
|
|
3981
|
+
* @return {string}
|
|
3982
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
3983
|
+
*/
|
|
3984
|
+
visibilityRefsKey() {
|
|
3985
|
+
return JSON.stringify(this.visibilityRefs)
|
|
3986
|
+
},
|
|
3987
|
+
|
|
3988
|
+
/**
|
|
3989
|
+
* What a grid widget's `visibleWhen` reads: the record, with the
|
|
3990
|
+
* `visibilityContext` records under `@context.<name>`. Null until the record loads.
|
|
3991
|
+
*
|
|
3992
|
+
* @return {object|null}
|
|
3993
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
|
|
3994
|
+
*/
|
|
3995
|
+
widgetVisibilityData() {
|
|
3996
|
+
return widgetConditionData(this.resolvedObject, this.visibilityRows)
|
|
3997
|
+
},
|
|
3998
|
+
|
|
3999
|
+
/**
|
|
4000
|
+
* The grid widgets whose `visibleWhen` needs a request (endpoint or
|
|
4001
|
+
* source), with what they ask, as one key: a change re-asks them.
|
|
4002
|
+
*
|
|
4003
|
+
* @return {string}
|
|
4004
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
|
|
4005
|
+
*/
|
|
4006
|
+
remoteWidgetConditionsKey() {
|
|
4007
|
+
if (!this.widgetVisibilityData || this.visibilityRowsPending) {
|
|
4008
|
+
return ''
|
|
4009
|
+
}
|
|
4010
|
+
const defs = [...(this.bodyGridWidgets || []), ...((this.activeView && this.activeView.widgets) || [])]
|
|
4011
|
+
const remote = defs.filter((def) => def && def.visibleWhen && typeof def.visibleWhen === 'object'
|
|
4012
|
+
&& !isLocallyDecidableVisibleWhen(def.visibleWhen))
|
|
4013
|
+
if (remote.length === 0) {
|
|
4014
|
+
return ''
|
|
4015
|
+
}
|
|
4016
|
+
return JSON.stringify([String(this.objectId || ''), remote.map((def) => [def.id, def.visibleWhen])])
|
|
4017
|
+
},
|
|
4018
|
+
|
|
3907
4019
|
/**
|
|
3908
4020
|
* The `@self` block of the resolved object (interaction markers live here).
|
|
3909
4021
|
*
|
|
@@ -4436,6 +4548,22 @@ export default {
|
|
|
4436
4548
|
},
|
|
4437
4549
|
|
|
4438
4550
|
watch: {
|
|
4551
|
+
// The records a condition names are read again for another record.
|
|
4552
|
+
visibilityRefsKey: {
|
|
4553
|
+
immediate: true,
|
|
4554
|
+
handler() {
|
|
4555
|
+
this.refreshVisibilityRows()
|
|
4556
|
+
},
|
|
4557
|
+
},
|
|
4558
|
+
|
|
4559
|
+
// Endpoint and source conditions on grid widgets are asked once per record.
|
|
4560
|
+
remoteWidgetConditionsKey: {
|
|
4561
|
+
immediate: true,
|
|
4562
|
+
handler() {
|
|
4563
|
+
this.refreshWidgetConditions()
|
|
4564
|
+
},
|
|
4565
|
+
},
|
|
4566
|
+
|
|
4439
4567
|
// The header's count pills count again for another record or other lists.
|
|
4440
4568
|
pillCountKey: {
|
|
4441
4569
|
immediate: true,
|
|
@@ -5674,6 +5802,62 @@ export default {
|
|
|
5674
5802
|
|| undefined
|
|
5675
5803
|
},
|
|
5676
5804
|
|
|
5805
|
+
/**
|
|
5806
|
+
* Read the records `visibilityContext` names for the loaded record. A
|
|
5807
|
+
* late answer for a record the page has left is dropped.
|
|
5808
|
+
*
|
|
5809
|
+
* @return {Promise<void>}
|
|
5810
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
|
|
5811
|
+
*/
|
|
5812
|
+
async refreshVisibilityRows() {
|
|
5813
|
+
const key = this.visibilityRefsKey
|
|
5814
|
+
const refs = this.visibilityRefs
|
|
5815
|
+
const seq = ++this.visibilityRowsSeq
|
|
5816
|
+
if (refs.length === 0) {
|
|
5817
|
+
// Write only on a change, so a page without `visibilityContext`
|
|
5818
|
+
// never touches the state its layout could read.
|
|
5819
|
+
if (Object.keys(this.visibilityRows).length > 0) {
|
|
5820
|
+
this.visibilityRows = {}
|
|
5821
|
+
}
|
|
5822
|
+
if (this.visibilityRowsFor !== key) {
|
|
5823
|
+
this.visibilityRowsFor = key
|
|
5824
|
+
}
|
|
5825
|
+
return
|
|
5826
|
+
}
|
|
5827
|
+
if (!this._visibilityRowCache) {
|
|
5828
|
+
this._visibilityRowCache = new Map()
|
|
5829
|
+
}
|
|
5830
|
+
const rows = await fetchVisibilityRows(refs, this._visibilityRowCache)
|
|
5831
|
+
if (seq !== this.visibilityRowsSeq) {
|
|
5832
|
+
return
|
|
5833
|
+
}
|
|
5834
|
+
this.visibilityRows = rows
|
|
5835
|
+
this.visibilityRowsFor = key
|
|
5836
|
+
},
|
|
5837
|
+
|
|
5838
|
+
/**
|
|
5839
|
+
* Ask the endpoint and source `visibleWhen` conditions of the grid
|
|
5840
|
+
* widgets, once per record. Each is fail-safe: an error hides the widget.
|
|
5841
|
+
*
|
|
5842
|
+
* @return {Promise<void>}
|
|
5843
|
+
* @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record
|
|
5844
|
+
*/
|
|
5845
|
+
async refreshWidgetConditions() {
|
|
5846
|
+
const seq = ++this.widgetConditionSeq
|
|
5847
|
+
if (this.remoteWidgetConditionsKey === '') {
|
|
5848
|
+
return
|
|
5849
|
+
}
|
|
5850
|
+
const [, entries] = JSON.parse(this.remoteWidgetConditionsKey)
|
|
5851
|
+
const ctx = { objectId: this.objectId, object: this.widgetVisibilityData || undefined }
|
|
5852
|
+
const answers = {}
|
|
5853
|
+
await Promise.all(entries.map(async ([id, cond]) => {
|
|
5854
|
+
answers[id] = await evaluateVisibleWhen(cond, ctx)
|
|
5855
|
+
}))
|
|
5856
|
+
if (seq === this.widgetConditionSeq) {
|
|
5857
|
+
this.widgetConditionAnswers = answers
|
|
5858
|
+
}
|
|
5859
|
+
},
|
|
5860
|
+
|
|
5677
5861
|
/**
|
|
5678
5862
|
* Drop the items that draw nothing, then close the rows they freed —
|
|
5679
5863
|
* `float: true` leaves the item below pinned under a gap nobody owns.
|
|
@@ -5696,7 +5880,10 @@ export default {
|
|
|
5696
5880
|
* Whether a layout item has anything to draw, so a grid cell is worth
|
|
5697
5881
|
* spending on it.
|
|
5698
5882
|
*
|
|
5699
|
-
*
|
|
5883
|
+
* A widget whose definition carries `visibleWhen` draws only while the
|
|
5884
|
+
* condition holds for the record (see `detailWidgetVisible`).
|
|
5885
|
+
*
|
|
5886
|
+
* Beyond that, TWO widget types are judged, and no others.
|
|
5700
5887
|
*
|
|
5701
5888
|
* `integration`, in the case where nothing can ever appear: a leaf from
|
|
5702
5889
|
* another app that is not registered, with no `requiredApp` to fall back
|
|
@@ -5743,6 +5930,18 @@ export default {
|
|
|
5743
5930
|
if (!widget) {
|
|
5744
5931
|
return true
|
|
5745
5932
|
}
|
|
5933
|
+
// The widget's own `visibleWhen`, read against the record and the
|
|
5934
|
+
// records `visibilityContext` names (a case's case type). Without
|
|
5935
|
+
// one the widget shows, as it always did, and the layout takes no
|
|
5936
|
+
// dependency on the record: a page without conditions re-lays out
|
|
5937
|
+
// exactly as often as before.
|
|
5938
|
+
if (widget.visibleWhen !== undefined && widget.visibleWhen !== null
|
|
5939
|
+
&& !detailWidgetVisible(widget, this.widgetVisibilityData, {
|
|
5940
|
+
pending: this.visibilityRowsPending,
|
|
5941
|
+
answers: this.widgetConditionAnswers,
|
|
5942
|
+
})) {
|
|
5943
|
+
return false
|
|
5944
|
+
}
|
|
5746
5945
|
// The board look shows the activity as the History tab. When a tabs
|
|
5747
5946
|
// widget already holds it, it is not also a body section.
|
|
5748
5947
|
if (this.isBoard && isActivityWidget(widget) && this.tabbedWidgetIds.has(widget.id)) {
|
|
@@ -111,6 +111,14 @@
|
|
|
111
111
|
role="note"
|
|
112
112
|
:data-testid="`cn-field-list-band-${row.key}`">
|
|
113
113
|
<span class="cn-field-list-widget__band-text">{{ row.detail }}</span>
|
|
114
|
+
<NcButton
|
|
115
|
+
v-for="copy in row.copies"
|
|
116
|
+
:key="`copy-${copy.path}`"
|
|
117
|
+
variant="tertiary"
|
|
118
|
+
:data-testid="`cn-field-list-copy-${copy.path}-${row.key}`"
|
|
119
|
+
@click="copyValue(copy)">
|
|
120
|
+
{{ copy.label }}
|
|
121
|
+
</NcButton>
|
|
114
122
|
<NcButton
|
|
115
123
|
v-for="action in row.bandActions"
|
|
116
124
|
:key="action.id"
|
|
@@ -162,6 +170,16 @@
|
|
|
162
170
|
<p v-if="row.detail" class="cn-field-list-widget__detail">
|
|
163
171
|
{{ row.detail }}
|
|
164
172
|
</p>
|
|
173
|
+
<div v-if="row.detail && row.copies.length > 0" class="cn-field-list-widget__copies">
|
|
174
|
+
<NcButton
|
|
175
|
+
v-for="copy in row.copies"
|
|
176
|
+
:key="`copy-${copy.path}`"
|
|
177
|
+
variant="tertiary"
|
|
178
|
+
:data-testid="`cn-field-list-copy-${copy.path}-${row.key}`"
|
|
179
|
+
@click="copyValue(copy)">
|
|
180
|
+
{{ copy.label }}
|
|
181
|
+
</NcButton>
|
|
182
|
+
</div>
|
|
165
183
|
</li>
|
|
166
184
|
</ul>
|
|
167
185
|
|
|
@@ -211,6 +229,7 @@
|
|
|
211
229
|
</NcButton>
|
|
212
230
|
</form>
|
|
213
231
|
|
|
232
|
+
<span class="cn-field-list-widget__sr" aria-live="polite" data-testid="cn-field-list-copied">{{ copied }}</span>
|
|
214
233
|
<NcNoteCard v-if="notice" :type="noticeType" data-testid="cn-field-list-notice">
|
|
215
234
|
{{ notice }}
|
|
216
235
|
</NcNoteCard>
|
|
@@ -260,7 +279,7 @@ export default {
|
|
|
260
279
|
*
|
|
261
280
|
* - `field` (required) — the object field holding the array.
|
|
262
281
|
* - `key` — the item field that identifies an entry (default: the item itself for strings).
|
|
263
|
-
* - `row` — `{ title, titleFallback?, avatar?, meta?: [paths|{ path, map?, format? }], pill?: { path, map?, variantMap? }, aside?: { path, format?, prefix?, fallback? }, detail?: { template, when
|
|
282
|
+
* - `row` — `{ title, titleFallback?, avatar?, meta?: [paths|{ path, map?, format? }], pill?: { path, map?, variantMap? }, aside?: { path, format?, prefix?, fallback? }, detail?: { template, when?, copy?: [{ path, label }] } }`. `detail.copy` puts a copy button beside the detail line for each named value of the row (an empty value gets none), such as a DNS record's name and value.
|
|
264
283
|
* - `add` — `{ label?, fields: [{ key, label, type?: 'text'|'select', options?, required?, pattern?, patternMessage? }], unique?, defaults? }`.
|
|
265
284
|
* - `remove` — `{ label? }`; omitted = no remove.
|
|
266
285
|
* - `rowActions` — `[{ id, label, when?, placement?: 'band', request: { method?, url, payload? } }]`; tokens `@object.<field>`, `@objectId`, `@row.<field>`.
|
|
@@ -316,6 +335,8 @@ export default {
|
|
|
316
335
|
busy: '',
|
|
317
336
|
notice: '',
|
|
318
337
|
noticeType: 'error',
|
|
338
|
+
// What the last copy button put on the clipboard, said to a screen reader.
|
|
339
|
+
copied: '',
|
|
319
340
|
}
|
|
320
341
|
},
|
|
321
342
|
|
|
@@ -358,6 +379,7 @@ export default {
|
|
|
358
379
|
pill: this.pillOf(entry, spec.pill),
|
|
359
380
|
aside: spec.aside ? this.asideOf(entry, spec.aside) : '',
|
|
360
381
|
detail: spec.detail && this.matches(entry, spec.detail.when) ? this.fill(spec.detail.template, entry) : '',
|
|
382
|
+
copies: spec.detail && this.matches(entry, spec.detail.when) ? this.copiesOf(entry, spec.detail.copy) : [],
|
|
361
383
|
actions: this.actionsFor(entry),
|
|
362
384
|
cells: this.isTable ? this.cellsOf(entry) : [],
|
|
363
385
|
}
|
|
@@ -818,6 +840,40 @@ export default {
|
|
|
818
840
|
* @return {Promise<void>}
|
|
819
841
|
* @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-row-can-run-a-configured-request
|
|
820
842
|
*/
|
|
843
|
+
/**
|
|
844
|
+
* The copy buttons of a row's detail line: one per named value that is not empty.
|
|
845
|
+
*
|
|
846
|
+
* @param {object} entry The entry.
|
|
847
|
+
* @param {Array<{path: string, label?: string}>|undefined} declared The `detail.copy` list.
|
|
848
|
+
* @return {Array<{path: string, label: string, value: string}>}
|
|
849
|
+
*/
|
|
850
|
+
copiesOf(entry, declared) {
|
|
851
|
+
return (Array.isArray(declared) ? declared : [])
|
|
852
|
+
.filter((copy) => copy && typeof copy.path === 'string' && copy.path !== '')
|
|
853
|
+
.map((copy) => ({
|
|
854
|
+
path: copy.path,
|
|
855
|
+
label: this.tr(copy.label || 'Copy'),
|
|
856
|
+
value: String(readPath(entry, copy.path) ?? ''),
|
|
857
|
+
}))
|
|
858
|
+
.filter((copy) => copy.value !== '')
|
|
859
|
+
},
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Put one value on the clipboard and say so; a browser that refuses says that instead.
|
|
863
|
+
*
|
|
864
|
+
* @param {{label: string, value: string}} copy The copy button.
|
|
865
|
+
* @return {Promise<void>}
|
|
866
|
+
*/
|
|
867
|
+
async copyValue(copy) {
|
|
868
|
+
this.copied = ''
|
|
869
|
+
try {
|
|
870
|
+
await navigator.clipboard.writeText(copy.value)
|
|
871
|
+
this.copied = `${this.tr('Copied')}: ${copy.label}`
|
|
872
|
+
} catch {
|
|
873
|
+
this.notify(this.tr('Copying did not work. Select the text and copy it yourself.'))
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
|
|
821
877
|
async runAction(action, row) {
|
|
822
878
|
this.busy = row.key
|
|
823
879
|
this.notice = ''
|
|
@@ -912,6 +968,12 @@ export default {
|
|
|
912
968
|
|
|
913
969
|
.cn-field-list-widget__meta,
|
|
914
970
|
.cn-field-list-widget__aside,
|
|
971
|
+
.cn-field-list-widget__copies {
|
|
972
|
+
display: flex;
|
|
973
|
+
flex-wrap: wrap;
|
|
974
|
+
gap: 4px;
|
|
975
|
+
}
|
|
976
|
+
|
|
915
977
|
.cn-field-list-widget__detail {
|
|
916
978
|
font-size: 13px;
|
|
917
979
|
}
|
|
@@ -280,7 +280,7 @@ export default {
|
|
|
280
280
|
* @return {string} The formatted string.
|
|
281
281
|
*/
|
|
282
282
|
formatNumber(value) {
|
|
283
|
-
return formatMetricValue(value, this.resolvedFormat, this.configCtx)
|
|
283
|
+
return formatMetricValue(value, this.resolvedFormat, this.configCtx, null, this.effectiveTranslate)
|
|
284
284
|
},
|
|
285
285
|
|
|
286
286
|
/**
|
|
@@ -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
|
*
|
|
@@ -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
|
/**
|