@conduction/nextcloud-vue 2.147.0 → 2.149.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 +24 -7
- 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/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +12 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +2 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +23 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +34 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +71 -8
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -2
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js +1 -1
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js +1 -1
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js +64 -8
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js.map +1 -1
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js +14 -4
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +32 -4
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +10 -2
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +84 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +61 -33
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue.js +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +49 -2
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js +90 -59
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue2.js +3 -1
- package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +10 -0
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +11 -0
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +28 -21
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +8 -0
- 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/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +3 -15
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +123 -115
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +4 -0
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue.js +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue2.js.map +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue3.js.map +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js +3 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js +26 -15
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +3 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +3 -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 +785 -518
- package/dist/esm/utils/builtInFormatters.js +53 -1
- package/dist/esm/utils/builtInFormatters.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 +2688 -2154
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +785 -518
- package/l10n/en.json +3 -1
- package/l10n/nl.json +3 -1
- package/package.json +1 -1
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +24 -7
- package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +13 -1
- package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +29 -2
- package/src/components/CnDataTable/CnDataTable.vue +34 -1
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +73 -10
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +6 -1
- package/src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue +1 -0
- package/src/components/CnDuplicateWarning/CnDuplicateWarning.vue +74 -10
- package/src/components/CnFactsWidget/CnFactsWidget.vue +41 -4
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +131 -3
- package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +103 -5
- package/src/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue +3 -1
- package/src/components/CnNotesCard/CnNotesCard.vue +10 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +16 -0
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +8 -0
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +19 -19
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +4 -0
- package/src/components/CnStepper/CnStepper.vue +10 -2
- package/src/css/dialog.css +22 -2
- package/src/css/look-board-detail.css +52 -3
- package/src/css/look-board-index.css +51 -1
- package/src/css/look-board.css +48 -5
- package/src/css/table.css +5 -2
- package/src/dialogs/CnAccessPurposeDialog.vue +27 -12
- package/src/schemas/app-manifest-v2.schema.json +4 -2
- package/src/utils/builtInFormatters.js +52 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -227,6 +227,9 @@
|
|
|
227
227
|
<CnIcon v-if="link.icon" :name="link.icon" :size="18" />
|
|
228
228
|
{{ link.label }}
|
|
229
229
|
</component>
|
|
230
|
+
<p v-if="link && link.note && record" class="cn-facts-widget__link-note" data-testid="cn-facts-link-note">
|
|
231
|
+
{{ link.note }}
|
|
232
|
+
</p>
|
|
230
233
|
<button
|
|
231
234
|
v-if="action && record"
|
|
232
235
|
type="button"
|
|
@@ -262,6 +265,7 @@ import { useRefLabels } from '../../composables/useRefLabels.js'
|
|
|
262
265
|
import { dispatchAction } from '../../utils/actionsDispatcher.js'
|
|
263
266
|
import { isBadgeVariant } from '../../utils/badgeVariants.js'
|
|
264
267
|
import { formatBoardDate } from '../../utils/boardDate.js'
|
|
268
|
+
import { formatTermDuration } from '../../utils/builtInFormatters.js'
|
|
265
269
|
import { conditionalLine } from '../../utils/conditionalLine.js'
|
|
266
270
|
import { parseDateValue } from '../../utils/dateVariant.js'
|
|
267
271
|
import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
|
|
@@ -485,7 +489,7 @@ export default {
|
|
|
485
489
|
* - `headerPill` — a status pill in the card header (reported through the `header` event): `{ path?, text?, map?, variant?, variantMap?, colorMap? }`, read from the record like a `pill` entry (DqBezwaar "Advies ontvangen"); nothing shows while the value is empty.
|
|
486
490
|
* - `notice` — `{ text, variant? }`, a note band inside the card under the facts (`info` by default; `success`, `warning` or `error`); the text is translated.
|
|
487
491
|
* - `footnote` — a plain muted line under the facts, without a band (DqZaakBesluiten "Na het ondertekenen ..."); the text is translated.
|
|
488
|
-
* `format` is `text` (default), `date` (board short date), `date-long` (always with the year), `longdate` (the month written out, the year only outside this year: "3 oktober"), `year` ("2026"), `list` (an array joined by ", ", each item read at `itemPath` and written in `itemFormat`), `group` (a Nextcloud group by its display name), `bytes` (a size, "1.5 MB"), `number` (in the reader's language, `decimals` fraction digits when set), `months` (a number of months in words, whole years as years: 240 is "20 jaar") or `currency` (a money amount in the reader's language, "€ 1.234,50": the code in `currency`, else the record's `currencyField`, else the app's reporting currency, else EUR; `decimals` defaults to 2).
|
|
492
|
+
* `format` is `text` (default), `date` (board short date), `date-long` (always with the year), `longdate` (the month written out, the year only outside this year: "3 oktober"), `duration` (an ISO period in words, "4 weken"; `extendBy` names the extension period on the record: "4 weken, te verdagen met 2"), `year` ("2026"), `list` (an array joined by ", ", each item read at `itemPath` and written in `itemFormat`), `group` (a Nextcloud group by its display name), `bytes` (a size, "1.5 MB"), `number` (in the reader's language, `decimals` fraction digits when set), `months` (a number of months in words, whole years as years: 240 is "20 jaar") or `currency` (a money amount in the reader's language, "€ 1.234,50": the code in `currency`, else the record's `currencyField`, else the app's reporting currency, else EUR; `decimals` defaults to 2).
|
|
489
493
|
* `map` turns a raw value into a label (translated). `parts` (on a `line`) lists paths, or `{ path, label?, format? }`, joined by `separator` (default " · "); empty parts are dropped.
|
|
490
494
|
* - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object. `{ register, schema, filter, sort? }` without `id` reads the first object that matches `filter` in `sort` order (`{ field, dir }`): the latest count of a shift. An entry with `from: "object"` reads the current object even then (a client card that also shows the ticket's channel).
|
|
491
495
|
* - `endpointSource` — `{ url, method?, params?, responsePath? }`, the block a stat or chart takes: the facts read an app endpoint, shared and cached like those.
|
|
@@ -866,18 +870,19 @@ export default {
|
|
|
866
870
|
if (!link || typeof link !== 'object' || !link.label) {
|
|
867
871
|
return null
|
|
868
872
|
}
|
|
873
|
+
const linkView = this.linkWords(link)
|
|
869
874
|
if (typeof link.sidebarTab === 'string' && link.sidebarTab !== '') {
|
|
870
875
|
return this.objectSidebarState
|
|
871
|
-
? {
|
|
876
|
+
? { ...linkView, sidebarTab: link.sidebarTab, icon: typeof link.icon === 'string' ? link.icon : '' }
|
|
872
877
|
: null
|
|
873
878
|
}
|
|
874
879
|
if (link.route) {
|
|
875
880
|
const to = this.resolveRoute(link.route, this.record)
|
|
876
|
-
return to ? {
|
|
881
|
+
return to ? { ...linkView, to } : null
|
|
877
882
|
}
|
|
878
883
|
if (typeof link.href === 'string' && link.href !== '') {
|
|
879
884
|
const href = this.linkHref(link, this.record)
|
|
880
|
-
return href ? {
|
|
885
|
+
return href ? { ...linkView, href, target: link.target === '_blank' ? '_blank' : '', icon: typeof link.icon === 'string' ? link.icon : '' } : null
|
|
881
886
|
}
|
|
882
887
|
return null
|
|
883
888
|
},
|
|
@@ -1016,6 +1021,23 @@ export default {
|
|
|
1016
1021
|
},
|
|
1017
1022
|
|
|
1018
1023
|
methods: {
|
|
1024
|
+
/**
|
|
1025
|
+
* A link's label and the muted note under it, their `{path}` and
|
|
1026
|
+
* `{path:format}` placeholders filled from the record ("{page:last-segment}").
|
|
1027
|
+
*
|
|
1028
|
+
* @param {object} link The configured link.
|
|
1029
|
+
* @return {{label: string, note: string}}
|
|
1030
|
+
* @spec openspec/changes/facts-link-label-from-record/specs/facts-widget/spec.md#requirement-a-facts-link-can-take-its-words-from-the-record
|
|
1031
|
+
*/
|
|
1032
|
+
linkWords(link) {
|
|
1033
|
+
const fill = (text) => (text.includes('{') && this.record ? this.templateValue(this.record, text) : this.tr(text))
|
|
1034
|
+
const label = fill(String(link.label))
|
|
1035
|
+
return {
|
|
1036
|
+
label: label || this.tr(String(link.label)),
|
|
1037
|
+
note: typeof link.note === 'string' && link.note !== '' ? fill(link.note) : '',
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
|
|
1019
1041
|
/**
|
|
1020
1042
|
* Open the page's object sidebar on one of its tabs.
|
|
1021
1043
|
*
|
|
@@ -1893,7 +1915,16 @@ export default {
|
|
|
1893
1915
|
const date = parseDateValue(raw)
|
|
1894
1916
|
return date === null ? '' : String(date.getFullYear())
|
|
1895
1917
|
}
|
|
1918
|
+
// A term in words, with the extension at `extendBy` on the record: "4 weken, te verdagen met 2".
|
|
1919
|
+
if (format === 'duration') {
|
|
1920
|
+
return formatTermDuration(raw, typeof spec.extendBy === 'string' && record ? readPath(record, spec.extendBy) : null)
|
|
1921
|
+
}
|
|
1896
1922
|
// The month written out, the year only outside this year: "3 oktober".
|
|
1923
|
+
// One segment of a path ("Team/Page"): its first or its last.
|
|
1924
|
+
if (format === 'first-segment' || format === 'last-segment') {
|
|
1925
|
+
const segments = String(raw).split('/').map((part) => part.trim()).filter((part) => part !== '')
|
|
1926
|
+
return (format === 'first-segment' ? segments[0] : segments[segments.length - 1]) || ''
|
|
1927
|
+
}
|
|
1897
1928
|
if (format === 'longdate') {
|
|
1898
1929
|
return formatLongDate(raw)
|
|
1899
1930
|
}
|
|
@@ -2310,6 +2341,12 @@ export default {
|
|
|
2310
2341
|
text-decoration: underline;
|
|
2311
2342
|
}
|
|
2312
2343
|
|
|
2344
|
+
.cn-facts-widget__link-note {
|
|
2345
|
+
margin: 0;
|
|
2346
|
+
color: var(--color-text-maxcontrast);
|
|
2347
|
+
font-size: 13px;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2313
2350
|
.cn-facts-widget__link--button {
|
|
2314
2351
|
display: inline-flex;
|
|
2315
2352
|
align-items: center;
|
|
@@ -165,13 +165,28 @@
|
|
|
165
165
|
</NcButton>
|
|
166
166
|
</div>
|
|
167
167
|
</div>
|
|
168
|
-
<ul v-else class="cn-field-list-widget__rows">
|
|
168
|
+
<ul v-else class="cn-field-list-widget__rows" :class="{ 'cn-field-list-widget__rows--options': isOptions }">
|
|
169
169
|
<li
|
|
170
170
|
v-for="row in rows"
|
|
171
171
|
:key="row.key"
|
|
172
172
|
class="cn-field-list-widget__row"
|
|
173
|
-
:
|
|
173
|
+
:draggable="canReorder ? 'true' : undefined"
|
|
174
|
+
:data-testid="`cn-field-list-row-${row.key}`"
|
|
175
|
+
@dragstart="dragFrom = row.index"
|
|
176
|
+
@dragover.prevent
|
|
177
|
+
@drop.prevent="dropOn(row.index)">
|
|
174
178
|
<div class="cn-field-list-widget__line">
|
|
179
|
+
<!-- `variant: "options"`: a grip that moves the option, by drag or with the arrow keys. -->
|
|
180
|
+
<button
|
|
181
|
+
v-if="canReorder"
|
|
182
|
+
type="button"
|
|
183
|
+
class="cn-field-list-widget__grip"
|
|
184
|
+
:aria-label="moveLabel(row)"
|
|
185
|
+
:data-testid="`cn-field-list-grip-${row.key}`"
|
|
186
|
+
@keydown.up.prevent="move(row.index, -1)"
|
|
187
|
+
@keydown.down.prevent="move(row.index, 1)">
|
|
188
|
+
<DragVertical :size="16" />
|
|
189
|
+
</button>
|
|
175
190
|
<span v-if="row.initials" class="cn-field-list-widget__avatar" aria-hidden="true">{{ row.initials }}</span>
|
|
176
191
|
<span class="cn-field-list-widget__main">
|
|
177
192
|
<a
|
|
@@ -187,6 +202,7 @@
|
|
|
187
202
|
:label="row.pill.label"
|
|
188
203
|
:variant="row.pill.variant"
|
|
189
204
|
size="small" />
|
|
205
|
+
<span v-if="row.code" class="cn-field-list-widget__code" :data-testid="`cn-field-list-code-${row.key}`">{{ row.code }}</span>
|
|
190
206
|
<span v-if="row.aside" class="cn-field-list-widget__aside">{{ row.aside }}</span>
|
|
191
207
|
<NcButton
|
|
192
208
|
v-for="action in row.actions"
|
|
@@ -307,7 +323,9 @@
|
|
|
307
323
|
</template>
|
|
308
324
|
|
|
309
325
|
<script>
|
|
326
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
310
327
|
import { NcActionButton, NcActions, NcButton, NcCheckboxRadioSwitch, NcNoteCard, NcSelect, NcTextField } from '@nextcloud/vue'
|
|
328
|
+
import DragVertical from 'vue-material-design-icons/DragVertical.vue'
|
|
311
329
|
import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
|
|
312
330
|
import { isBadgeVariant } from '../../utils/badgeVariants.js'
|
|
313
331
|
import { formatBoardDate } from '../../utils/boardDate.js'
|
|
@@ -368,7 +386,7 @@ function rowHref(entry, spec) {
|
|
|
368
386
|
export default {
|
|
369
387
|
name: 'CnFieldListWidget',
|
|
370
388
|
|
|
371
|
-
components: { CnStatusBadge, NcActionButton, NcActions, NcButton, NcCheckboxRadioSwitch, NcNoteCard, NcSelect, NcTextField },
|
|
389
|
+
components: { CnStatusBadge, DragVertical, NcActionButton, NcActions, NcButton, NcCheckboxRadioSwitch, NcNoteCard, NcSelect, NcTextField },
|
|
372
390
|
|
|
373
391
|
inject: {
|
|
374
392
|
cnTranslate: { default: () => (key) => key },
|
|
@@ -388,6 +406,7 @@ export default {
|
|
|
388
406
|
* - `rowActions` — `[{ id, label, ariaLabel?, when?, placement?: 'band'|'inline', request: { method?, url, payload? } }]`, or `{ id, label, dialog, props }` to open a modal registered in the app registry with filled props (shown in `readOnly` too, since it writes nothing, and only when every token in `props` fills); tokens `@object.<field>`, `@objectId`, `@row.<field>`, `@index`. In the table variant `placement: 'inline'` draws the action as a button in the row instead of in the "..." menu; `ariaLabel` is a template filled from the row.
|
|
389
407
|
* - `when` (row actions, `detail`) — `{ field: value }`, every field equal; or per field `{ eq?, neq?, set? }`, where a value `@row.<field>` (or `@<field>`) is another field of the same row: `{ deliveredRef: { set: true, neq: '@originalRef' } }`.
|
|
390
408
|
* - `rowSource` — `{ url, path?, key }`: live per-row state merged by key.
|
|
409
|
+
* - `variant: 'options'` — the rows as framed options (a field's choice list): a grip that moves the option by drag or with the arrow keys and saves the order, and a code chip from `code` (`{ path }` on an object option, or `{ field }`, a map on the record from a plain option to its code).
|
|
391
410
|
* - `variant: 'table'` + `columns` — `[{ header, path, map?, format?, strong?, pill?, variantMap?, labelsFrom? }]` (a list value reads as its items joined; `labelsFrom` names an add field whose option labels word the values): rows as a table; a per-row "..." menu holds the row actions and remove; `detail` lines become a warning band under the table, with the row actions that have `placement: 'band'`.
|
|
392
411
|
* - `add.trigger: 'button'`, `add.placement: 'header' | 'title' | 'footer'` — the add form opens from a button; `title` reports the button for the card's title row on the `header` event.
|
|
393
412
|
* - `readOnly`, `hint`, `emptyText`.
|
|
@@ -436,6 +455,8 @@ export default {
|
|
|
436
455
|
return {
|
|
437
456
|
/** The list as last saved by this widget; null = read it off objectData. */
|
|
438
457
|
saved: null,
|
|
458
|
+
/** The place of the option being dragged, -1 while none is. */
|
|
459
|
+
dragFrom: -1,
|
|
439
460
|
/** Live per-row state from `rowSource`, by key. */
|
|
440
461
|
live: {},
|
|
441
462
|
draft: {},
|
|
@@ -500,10 +521,21 @@ export default {
|
|
|
500
521
|
copies: spec.detail && this.matches(entry, spec.detail.when) ? this.copiesOf(entry, spec.detail.copy) : [],
|
|
501
522
|
actions: this.actionsFor(entry, index),
|
|
502
523
|
cells: this.isTable ? this.cellsOf(entry) : [],
|
|
524
|
+
code: this.isOptions ? this.codeOf(item, entry) : '',
|
|
503
525
|
}
|
|
504
526
|
})
|
|
505
527
|
},
|
|
506
528
|
|
|
529
|
+
/** Whether the rows are drawn as framed options (`variant: 'options'`): a grip, the name, a code chip. */
|
|
530
|
+
isOptions() {
|
|
531
|
+
return this.content.variant === 'options'
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
/** Whether the options can be moved: the options look on a list that saves. */
|
|
535
|
+
canReorder() {
|
|
536
|
+
return this.isOptions && !this.readOnly && Boolean(this.content.field)
|
|
537
|
+
},
|
|
538
|
+
|
|
507
539
|
/** Whether the list is read-only. */
|
|
508
540
|
readOnly() {
|
|
509
541
|
return this.content.readOnly === true
|
|
@@ -1300,6 +1332,73 @@ export default {
|
|
|
1300
1332
|
* @param {object} row The row.
|
|
1301
1333
|
* @return {Promise<void>}
|
|
1302
1334
|
*/
|
|
1335
|
+
/**
|
|
1336
|
+
* An option's code chip: `content.code.path` on an object option, or the
|
|
1337
|
+
* value's entry in the record's map at `content.code.field` for a plain
|
|
1338
|
+
* one ("Woning" -> "WON"); '' for none.
|
|
1339
|
+
*
|
|
1340
|
+
* @param {unknown} item The stored option.
|
|
1341
|
+
* @param {object} entry The option as a row reads it.
|
|
1342
|
+
* @return {string}
|
|
1343
|
+
*/
|
|
1344
|
+
codeOf(item, entry) {
|
|
1345
|
+
const spec = this.content.code
|
|
1346
|
+
if (!spec || typeof spec !== 'object') {
|
|
1347
|
+
return ''
|
|
1348
|
+
}
|
|
1349
|
+
if (item && typeof item === 'object' && typeof spec.path === 'string') {
|
|
1350
|
+
return String(readPath(item, spec.path) ?? '')
|
|
1351
|
+
}
|
|
1352
|
+
const map = typeof spec.field === 'string' ? readPath(this.objectData || {}, spec.field) : null
|
|
1353
|
+
const code = map && typeof map === 'object' ? map[String(entry.value ?? item)] : ''
|
|
1354
|
+
return typeof code === 'string' ? code : ''
|
|
1355
|
+
},
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* The grip's accessible name.
|
|
1359
|
+
*
|
|
1360
|
+
* @param {object} row The row.
|
|
1361
|
+
* @return {string}
|
|
1362
|
+
*/
|
|
1363
|
+
moveLabel(row) {
|
|
1364
|
+
return t('nextcloud-vue', 'Move {item}, use the arrow keys', { item: row.title })
|
|
1365
|
+
},
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Drop the dragged option on this one's place.
|
|
1369
|
+
*
|
|
1370
|
+
* @param {number} to The place it lands on.
|
|
1371
|
+
* @return {Promise<void>}
|
|
1372
|
+
*/
|
|
1373
|
+
async dropOn(to) {
|
|
1374
|
+
const from = this.dragFrom
|
|
1375
|
+
this.dragFrom = -1
|
|
1376
|
+
if (from < 0 || from === to) {
|
|
1377
|
+
return
|
|
1378
|
+
}
|
|
1379
|
+
await this.move(from, to - from)
|
|
1380
|
+
},
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Move an option by a number of places and save the list.
|
|
1384
|
+
*
|
|
1385
|
+
* @param {number} from Its place.
|
|
1386
|
+
* @param {number} by How far, negative is up.
|
|
1387
|
+
* @return {Promise<void>}
|
|
1388
|
+
*/
|
|
1389
|
+
async move(from, by) {
|
|
1390
|
+
const to = from + by
|
|
1391
|
+
if (!this.canReorder || to < 0 || to >= this.items.length) {
|
|
1392
|
+
return
|
|
1393
|
+
}
|
|
1394
|
+
await this.save((stored) => {
|
|
1395
|
+
const list = [...stored]
|
|
1396
|
+
const [moved] = list.splice(from, 1)
|
|
1397
|
+
list.splice(to, 0, moved)
|
|
1398
|
+
return list
|
|
1399
|
+
}, this.keyOf(this.items[from], from))
|
|
1400
|
+
},
|
|
1401
|
+
|
|
1303
1402
|
async remove(row) {
|
|
1304
1403
|
await this.save((stored) => stored.filter((item, index) => this.keyOf(item, index) !== row.key), row.key)
|
|
1305
1404
|
},
|
|
@@ -1409,6 +1508,35 @@ export default {
|
|
|
1409
1508
|
padding-top: 0;
|
|
1410
1509
|
}
|
|
1411
1510
|
|
|
1511
|
+
/* `variant: "options"` (DqZaaktypeVelden Keuzelijst): each option a framed
|
|
1512
|
+
row, 6px apart, with a grip before its name and its code in a chip. */
|
|
1513
|
+
.cn-field-list-widget__rows--options {
|
|
1514
|
+
gap: 6px;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
.cn-field-list-widget__rows--options .cn-field-list-widget__row,
|
|
1518
|
+
.cn-field-list-widget__rows--options .cn-field-list-widget__row:first-child {
|
|
1519
|
+
padding: 6px 12px;
|
|
1520
|
+
border: 1px solid var(--color-border);
|
|
1521
|
+
border-radius: var(--border-radius-element, 8px);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
.cn-field-list-widget__grip {
|
|
1525
|
+
flex: none;
|
|
1526
|
+
display: inline-flex;
|
|
1527
|
+
padding: 0;
|
|
1528
|
+
border: 0;
|
|
1529
|
+
background: transparent;
|
|
1530
|
+
color: var(--color-text-maxcontrast);
|
|
1531
|
+
cursor: grab;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
.cn-field-list-widget__code {
|
|
1535
|
+
flex: none;
|
|
1536
|
+
color: var(--color-text-maxcontrast);
|
|
1537
|
+
font-size: 13px;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1412
1540
|
.cn-field-list-widget__line {
|
|
1413
1541
|
display: flex;
|
|
1414
1542
|
align-items: center;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- SPDX-License-Identifier: EUPL-1.2
|
|
4
4
|
-->
|
|
5
5
|
<template>
|
|
6
|
-
<div class="cn-flow-runs-widget">
|
|
6
|
+
<div class="cn-flow-runs-widget" :class="{ 'cn-flow-runs-widget--pills': pills }">
|
|
7
7
|
<!-- One quiet line on failure, never a leaked axios status string
|
|
8
8
|
(ADR-062). The real error goes to the console. -->
|
|
9
9
|
<p v-if="error" class="cn-flow-runs-widget__error">
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
{{ emptyLabel }}
|
|
27
27
|
</p>
|
|
28
28
|
<template v-else>
|
|
29
|
-
<p v-if="rows.length === 0" class="cn-flow-runs-widget__empty">
|
|
29
|
+
<p v-if="rows.length === 0 && content.quietLineWithHistory !== false" class="cn-flow-runs-widget__empty">
|
|
30
30
|
{{ emptyLabel }}
|
|
31
31
|
</p>
|
|
32
32
|
<ul v-else class="cn-flow-runs-widget__list">
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
:href="runHref(run) || undefined"
|
|
43
43
|
@click="onRowClick($event, run)">
|
|
44
44
|
<span
|
|
45
|
+
v-if="!pills"
|
|
45
46
|
class="cn-flow-runs-widget__dot"
|
|
46
47
|
:class="`cn-flow-runs-widget__dot--${run.status}`"
|
|
47
48
|
:title="statusLabel(run.status)" />
|
|
@@ -49,6 +50,12 @@
|
|
|
49
50
|
<span class="cn-flow-runs-widget__name">{{ run.flowName }}</span>
|
|
50
51
|
<span class="cn-flow-runs-widget__meta">{{ metaLine(run) }}</span>
|
|
51
52
|
</span>
|
|
53
|
+
<CnStatusBadge
|
|
54
|
+
v-if="pills"
|
|
55
|
+
class="cn-flow-runs-widget__pill"
|
|
56
|
+
:label="statusLabel(run.status)"
|
|
57
|
+
:variant="statusVariant(run.status)"
|
|
58
|
+
size="small" />
|
|
52
59
|
<span class="cn-flow-runs-widget__age">{{ ageLabel(run) }}</span>
|
|
53
60
|
</component>
|
|
54
61
|
</li>
|
|
@@ -62,7 +69,7 @@
|
|
|
62
69
|
labelled section with a hollow dot and a muted name, so live
|
|
63
70
|
and finished never rely on colour alone to tell apart. -->
|
|
64
71
|
<template v-if="showHistory">
|
|
65
|
-
<p class="cn-flow-runs-widget__history-title">
|
|
72
|
+
<p v-if="content.historyTitle !== false" class="cn-flow-runs-widget__history-title">
|
|
66
73
|
{{ tr('Earlier runs') }}
|
|
67
74
|
</p>
|
|
68
75
|
<p v-if="completedError" class="cn-flow-runs-widget__error">
|
|
@@ -81,6 +88,7 @@
|
|
|
81
88
|
:href="runHref(run) || undefined"
|
|
82
89
|
@click="onRowClick($event, run)">
|
|
83
90
|
<span
|
|
91
|
+
v-if="!pills"
|
|
84
92
|
class="cn-flow-runs-widget__dot cn-flow-runs-widget__dot--terminal"
|
|
85
93
|
:class="`cn-flow-runs-widget__dot--${run.status}`"
|
|
86
94
|
:title="statusLabel(run.status)" />
|
|
@@ -88,6 +96,12 @@
|
|
|
88
96
|
<span class="cn-flow-runs-widget__name">{{ run.flowName }}</span>
|
|
89
97
|
<span class="cn-flow-runs-widget__meta">{{ metaLine(run) }}</span>
|
|
90
98
|
</span>
|
|
99
|
+
<CnStatusBadge
|
|
100
|
+
v-if="pills"
|
|
101
|
+
class="cn-flow-runs-widget__pill"
|
|
102
|
+
:label="statusLabel(run.status)"
|
|
103
|
+
:variant="statusVariant(run.status)"
|
|
104
|
+
size="small" />
|
|
91
105
|
<span class="cn-flow-runs-widget__age">{{ ageLabel(run) }}</span>
|
|
92
106
|
</component>
|
|
93
107
|
</li>
|
|
@@ -106,9 +120,11 @@ import { NcLoadingIcon } from '@nextcloud/vue'
|
|
|
106
120
|
import { computed, inject, ref } from 'vue'
|
|
107
121
|
import { useEndpointSource } from '../../composables/useEndpointSource.js'
|
|
108
122
|
import { dispatchAction } from '../../utils/actionsDispatcher.js'
|
|
123
|
+
import { formatBoardDate } from '../../utils/boardDate.js'
|
|
109
124
|
import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
|
|
110
125
|
import { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'
|
|
111
126
|
import { resolveDeepTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'
|
|
127
|
+
import { CnStatusBadge } from '../CnStatusBadge/index.js'
|
|
112
128
|
|
|
113
129
|
/**
|
|
114
130
|
* The OpenRegister endpoint that lists the caller's live flow runs.
|
|
@@ -197,6 +213,7 @@ export default {
|
|
|
197
213
|
name: 'CnFlowRunsWidget',
|
|
198
214
|
|
|
199
215
|
components: {
|
|
216
|
+
CnStatusBadge,
|
|
200
217
|
NcLoadingIcon,
|
|
201
218
|
},
|
|
202
219
|
|
|
@@ -231,6 +248,17 @@ export default {
|
|
|
231
248
|
* subject's finished runs render below it. Absent = today's org-wide
|
|
232
249
|
* widget, unchanged.
|
|
233
250
|
* - `emptyText` — override for the "nothing running" line.
|
|
251
|
+
* - `quietLineWithHistory` — `false` leaves out the "nothing running"
|
|
252
|
+
* line while the subject has finished runs to list.
|
|
253
|
+
* - `historyTitle` — `false` leaves out the "Earlier runs" heading.
|
|
254
|
+
* - `statusPill` — `true` shows the status as a pill on each row
|
|
255
|
+
* (no dot, and the meta line leaves the status out), rows ruled.
|
|
256
|
+
* - `statusLabels` — words per status (`{ completed: 'Succeeded' }`),
|
|
257
|
+
* translated, over the built-in ones.
|
|
258
|
+
* - `triggerLabels` — the meta line per trigger in words
|
|
259
|
+
* (`{ 'object.created': 'Started when the case was created' }`), translated.
|
|
260
|
+
* - `age` — `'datetime'` shows when the run started ("3 okt, 14:32")
|
|
261
|
+
* instead of how long ago ("13d").
|
|
234
262
|
* - `headerAction` — an action descriptor drawn in the card header
|
|
235
263
|
* (`{ label, icon?, ...action }`, "Flow uitvoeren"), reported through
|
|
236
264
|
* the `header` event and dispatched with the page's dispatcher; its
|
|
@@ -421,6 +449,11 @@ export default {
|
|
|
421
449
|
return this.tr('+{count} more').replace('{count}', String(this.hiddenCount))
|
|
422
450
|
},
|
|
423
451
|
|
|
452
|
+
/** Whether each row shows its status as a pill (`content.statusPill`). */
|
|
453
|
+
pills() {
|
|
454
|
+
return this.content.statusPill === true
|
|
455
|
+
},
|
|
456
|
+
|
|
424
457
|
/** The nothing-running line. */
|
|
425
458
|
emptyLabel() {
|
|
426
459
|
const override = this.content.emptyText
|
|
@@ -678,10 +711,29 @@ export default {
|
|
|
678
711
|
* @return {string} The label.
|
|
679
712
|
*/
|
|
680
713
|
statusLabel(status) {
|
|
681
|
-
const
|
|
714
|
+
const own = this.content.statusLabels && this.content.statusLabels[status]
|
|
715
|
+
const key = (typeof own === 'string' && own !== '') ? own : STATUS_LABELS[status]
|
|
682
716
|
return key ? this.tr(key) : String(status || '')
|
|
683
717
|
},
|
|
684
718
|
|
|
719
|
+
/**
|
|
720
|
+
* The pill tone of a status: finished well is success, failed is error,
|
|
721
|
+
* anything still going is info.
|
|
722
|
+
*
|
|
723
|
+
* @param {string} status The run status.
|
|
724
|
+
* @return {string} A CnStatusBadge variant.
|
|
725
|
+
* @spec openspec/changes/flow-runs-ruled-with-pills/specs/cn-flow-runs-widget/spec.md
|
|
726
|
+
*/
|
|
727
|
+
statusVariant(status) {
|
|
728
|
+
if (status === 'completed') {
|
|
729
|
+
return 'success'
|
|
730
|
+
}
|
|
731
|
+
if (status === 'failed' || status === 'dead_letter') {
|
|
732
|
+
return 'error'
|
|
733
|
+
}
|
|
734
|
+
return status === 'stopped' ? 'default' : 'info'
|
|
735
|
+
},
|
|
736
|
+
|
|
685
737
|
/**
|
|
686
738
|
* The secondary line: status, the step the run sits on, and what
|
|
687
739
|
* triggered it — whichever of those the run actually carries.
|
|
@@ -690,7 +742,11 @@ export default {
|
|
|
690
742
|
* @return {string} The meta line.
|
|
691
743
|
*/
|
|
692
744
|
metaLine(run) {
|
|
693
|
-
const
|
|
745
|
+
const words = this.content.triggerLabels && this.content.triggerLabels[run.trigger]
|
|
746
|
+
if (typeof words === 'string' && words !== '') {
|
|
747
|
+
return this.tr(words)
|
|
748
|
+
}
|
|
749
|
+
const parts = this.pills ? [] : [this.statusLabel(run.status)]
|
|
694
750
|
if (run.step) {
|
|
695
751
|
parts.push(String(run.step))
|
|
696
752
|
}
|
|
@@ -714,6 +770,11 @@ export default {
|
|
|
714
770
|
if (Number.isFinite(started) === false) {
|
|
715
771
|
return ''
|
|
716
772
|
}
|
|
773
|
+
if (this.content.age === 'datetime') {
|
|
774
|
+
const at = new Date(started)
|
|
775
|
+
const pad = (value) => String(value).padStart(2, '0')
|
|
776
|
+
return `${formatBoardDate(at)}, ${pad(at.getHours())}:${pad(at.getMinutes())}`
|
|
777
|
+
}
|
|
717
778
|
const seconds = Math.max(0, Math.round((Date.now() - started) / 1000))
|
|
718
779
|
if (seconds < 60) {
|
|
719
780
|
return this.tr('now')
|
|
@@ -965,6 +1026,43 @@ a.cn-flow-runs-widget__row-content:hover .cn-flow-runs-widget__name {
|
|
|
965
1026
|
letter-spacing: 0.04em;
|
|
966
1027
|
}
|
|
967
1028
|
|
|
1029
|
+
/* Status pills: each row ruled on top, the name over its meta line, the pill
|
|
1030
|
+
and the start moment at the end, as the Dq case boards draw the Flows card. */
|
|
1031
|
+
.cn-flow-runs-widget--pills .cn-flow-runs-widget__row {
|
|
1032
|
+
border-top: 1px solid var(--color-border);
|
|
1033
|
+
border-bottom: none;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
.cn-flow-runs-widget--pills .cn-flow-runs-widget__row-content {
|
|
1037
|
+
flex-wrap: wrap;
|
|
1038
|
+
gap: 6px 16px;
|
|
1039
|
+
padding: 11px 0;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
.cn-flow-runs-widget--pills .cn-flow-runs-widget__body {
|
|
1043
|
+
flex: 1 1 260px;
|
|
1044
|
+
gap: 2px;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
.cn-flow-runs-widget--pills .cn-flow-runs-widget__name,
|
|
1048
|
+
.cn-flow-runs-widget--pills .cn-flow-runs-widget__row--terminal .cn-flow-runs-widget__name {
|
|
1049
|
+
font-size: 15px;
|
|
1050
|
+
font-weight: 500;
|
|
1051
|
+
color: var(--color-main-text);
|
|
1052
|
+
white-space: normal;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.cn-flow-runs-widget--pills .cn-flow-runs-widget__meta {
|
|
1056
|
+
font-size: 13px;
|
|
1057
|
+
white-space: normal;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
.cn-flow-runs-widget--pills .cn-flow-runs-widget__age {
|
|
1061
|
+
flex: 0 0 104px;
|
|
1062
|
+
font-size: 13px;
|
|
1063
|
+
text-align: end;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
968
1066
|
@keyframes cn-flow-runs-pulse {
|
|
969
1067
|
0%, 100% { opacity: 1; }
|
|
970
1068
|
50% { opacity: 0.35; }
|
|
@@ -42,6 +42,8 @@ export default {
|
|
|
42
42
|
cnWidgetSelection: { from: WIDGET_SELECTION, default: null },
|
|
43
43
|
/** The host's translate (CnAppRoot provides `cnTranslate`), applied to the title before it is filled. */
|
|
44
44
|
cnTranslate: { default: () => (key) => key },
|
|
45
|
+
/** The app's named formatters (CnAppRoot provides `cnFormatters`), for a `{field|formatter}` token in the title. */
|
|
46
|
+
cnFormatters: { default: () => ({}) },
|
|
45
47
|
},
|
|
46
48
|
|
|
47
49
|
inheritAttrs: false,
|
|
@@ -93,7 +95,7 @@ export default {
|
|
|
93
95
|
return this.widget
|
|
94
96
|
}
|
|
95
97
|
const translated = this.cnTranslate(title)
|
|
96
|
-
return { ...this.widget, title: fillTemplate(translated, this.row) || translated.replace(/\s*\{[^}]*\}/g, '') }
|
|
98
|
+
return { ...this.widget, title: fillTemplate(translated, this.row, new Date(), { formatters: this.cnFormatters }) || translated.replace(/\s*\{[^}]*\}/g, '') }
|
|
97
99
|
},
|
|
98
100
|
},
|
|
99
101
|
}
|
|
@@ -176,6 +176,8 @@ import CnNoteComposer from '../CnNoteComposer/CnNoteComposer.vue'
|
|
|
176
176
|
import CnUserActionMenu from '../CnUserActionMenu/CnUserActionMenu.vue'
|
|
177
177
|
import CnVisibilityChip from '../CnVisibilityChip/CnVisibilityChip.vue'
|
|
178
178
|
import { useFileComments } from '../../composables/useFileComments.js'
|
|
179
|
+
import { normalizeLook } from '../../composables/useLook.js'
|
|
180
|
+
import { formatBoardDateTime } from '../../utils/fillTemplate.js'
|
|
179
181
|
import { buildHeaders, prefixUrl } from '../../utils/index.js'
|
|
180
182
|
import { initialsOf } from '../../utils/initials.js'
|
|
181
183
|
import { leafSourceUrl } from '../../utils/leafSourceUrl.js'
|
|
@@ -248,6 +250,10 @@ export default {
|
|
|
248
250
|
Delete,
|
|
249
251
|
},
|
|
250
252
|
|
|
253
|
+
inject: {
|
|
254
|
+
cnLook: { default: 'nextcloud' },
|
|
255
|
+
},
|
|
256
|
+
|
|
251
257
|
props: {
|
|
252
258
|
/** OpenRegister register ID */
|
|
253
259
|
registerId: {
|
|
@@ -795,6 +801,10 @@ export default {
|
|
|
795
801
|
if (!dateStr) {
|
|
796
802
|
return ''
|
|
797
803
|
}
|
|
804
|
+
// The board look writes a note's moment as the boards do: "6 okt 11:10", the year only outside this year.
|
|
805
|
+
if (normalizeLook(this.cnLook) === 'board') {
|
|
806
|
+
return formatBoardDateTime(dateStr) || dateStr
|
|
807
|
+
}
|
|
798
808
|
try {
|
|
799
809
|
return new Date(dateStr).toLocaleString(userLocale(), {
|
|
800
810
|
year: 'numeric',
|
|
@@ -112,6 +112,11 @@
|
|
|
112
112
|
:row="object" />
|
|
113
113
|
<template v-else>{{ recordFootCell.text }}</template>
|
|
114
114
|
</span>
|
|
115
|
+
<!-- An empty footerCell keeps its place as a dash, as an empty table cell. -->
|
|
116
|
+
<span
|
|
117
|
+
v-else-if="recordFootCellDeclared"
|
|
118
|
+
class="cn-object-card__record-foot-cell cn-object-card__record-foot-cell--empty"
|
|
119
|
+
data-testid="cn-object-card-foot-cell-empty">—</span>
|
|
115
120
|
<span
|
|
116
121
|
v-if="recordDate"
|
|
117
122
|
class="cn-object-card__record-date"
|
|
@@ -589,6 +594,17 @@ export default {
|
|
|
589
594
|
return { text: part.text, tone }
|
|
590
595
|
},
|
|
591
596
|
|
|
597
|
+
/**
|
|
598
|
+
* Whether the record layout names a `footerCell`, so an empty one
|
|
599
|
+
* keeps its place.
|
|
600
|
+
*
|
|
601
|
+
* @spec openspec/changes/card-empty-footer-cell-dash/specs/card-empty-footer-cell-dash/spec.md#requirement-an-empty-footer-cell-reads-as-a-dash
|
|
602
|
+
* @return {boolean}
|
|
603
|
+
*/
|
|
604
|
+
recordFootCellDeclared() {
|
|
605
|
+
return Boolean(this.recordLayout && this.recordLayout.footerCell)
|
|
606
|
+
},
|
|
607
|
+
|
|
592
608
|
/**
|
|
593
609
|
* The footer's `footerCell` part, read as a meta part is (through the
|
|
594
610
|
* page column it names), or null while it is empty.
|
|
@@ -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, 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. |
|
|
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. `format: 'duration'` reads an ISO period in words (`P28D` "4 weken"), and `extendBy` names the object's extension period, read as "4 weken, te verdagen met 2". |
|
|
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. |
|
|
@@ -438,10 +438,12 @@ import CnGroupNameCell from '../CnCellRenderer/CnGroupNameCell.vue'
|
|
|
438
438
|
import CnFormDialog from '../CnFormDialog/CnFormDialog.vue'
|
|
439
439
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
440
440
|
import { useObjectStore } from '../../store/index.js'
|
|
441
|
+
import { formatTermDuration } from '../../utils/builtInFormatters.js'
|
|
441
442
|
import { foldDottedFields, isDottedKey, spreadDottedFields } from '../../utils/dottedFields.js'
|
|
442
443
|
import { groupDisplayName, loadGroupDisplayName, searchNextcloudGroups } from '../../utils/groupAutocomplete.js'
|
|
443
444
|
import { humaniseKey } from '../../utils/humaniseKey.js'
|
|
444
445
|
import { PANEL_ACTION_SINK } from '../../utils/panelActions.js'
|
|
446
|
+
import { readPath } from '../../utils/readPath.js'
|
|
445
447
|
import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
446
448
|
import { saveEdited } from '../../utils/saveEdited.js'
|
|
447
449
|
import { fieldsFromSchema, formatValue } from '../../utils/schema.js'
|
|
@@ -1103,6 +1105,12 @@ export default {
|
|
|
1103
1105
|
// label, beside a list that already read "Ingediend".
|
|
1104
1106
|
const enumLabel = this.enumDisplayLabel(field, raw)
|
|
1105
1107
|
const empty = this.emptyValueFor(field, raw)
|
|
1108
|
+
if (empty === null && field.format === 'duration' && raw !== null && raw !== undefined && raw !== '') {
|
|
1109
|
+
// A term in words, with the extension at `extendBy` when the object holds one.
|
|
1110
|
+
const extension = typeof field.extendBy === 'string' ? readPath(this.objectData || {}, field.extendBy) : null
|
|
1111
|
+
values[field.key] = formatTermDuration(raw, extension)
|
|
1112
|
+
continue
|
|
1113
|
+
}
|
|
1106
1114
|
values[field.key] = empty !== null ? empty : (enumLabel !== null ? enumLabel : formatValue(raw, prop || {}))
|
|
1107
1115
|
}
|
|
1108
1116
|
return values
|
|
@@ -860,7 +860,7 @@ export default {
|
|
|
860
860
|
// `secondary` (a second, muted line under the cell) rides
|
|
861
861
|
// along too: the table draws it, and dropping it here took the
|
|
862
862
|
// list rows' second line away (zuiddrecht-pixel-gaps-3).
|
|
863
|
-
for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow', 'cellClass']) {
|
|
863
|
+
for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow', 'cellClass', 'emptyValue']) {
|
|
864
864
|
if (c[k] !== undefined) {
|
|
865
865
|
out[k] = c[k]
|
|
866
866
|
}
|