@conduction/nextcloud-vue 2.139.0 → 2.140.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/CnAppRoot/CnAppRoot.vue2.js +9 -2
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +17 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +2 -2
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +52 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +23 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +106 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +30 -7
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +59 -3
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +44 -34
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +28 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js +7 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +5 -0
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +28 -3
- 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/CnRecordNoteRows.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +10 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +1 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +12 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +1 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue.js +1 -1
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js +23 -1
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js.map +1 -1
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js +43 -13
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +5 -2
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useTaskInboxStore.js +4 -3
- package/dist/esm/composables/useTaskInboxStore.js.map +1 -1
- package/dist/esm/l10n/en.json.js +2 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +5 -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 +466 -371
- package/dist/esm/utils/builtInFormatters.js +23 -1
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +7 -1
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/myTeam.js +24 -4
- package/dist/esm/utils/myTeam.js.map +1 -1
- package/dist/esm/utils/resolveFilterTokens.js +5 -4
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/transitionInputs.js +43 -2
- package/dist/esm/utils/transitionInputs.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +771 -222
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +466 -371
- package/l10n/en.json +2 -1
- package/l10n/nl.json +5 -1
- package/package.json +1 -1
- package/src/components/CnAppRoot/CnAppRoot.vue +9 -2
- package/src/components/CnCellRenderer/CnCellRenderer.vue +19 -3
- package/src/components/CnChartWidget/CnChartWidget.md +1 -0
- package/src/components/CnChartWidget/CnChartWidget.vue +72 -1
- package/src/components/CnConversationThread/CnConversationWidget.vue +126 -1
- package/src/components/CnFactsWidget/CnFactsWidget.vue +72 -5
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +28 -1
- package/src/components/CnFilesBrowser/filesBrowser.js +7 -1
- package/src/components/CnFilesBrowser/filesBrowserColumns.js +5 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +30 -2
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +28 -3
- package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +10 -0
- package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +68 -2
- package/src/components/CnTransitionInputFields/CnTransitionInputFields.vue +43 -1
- package/src/composables/indexSources.js +5 -2
- package/src/composables/useTaskInboxStore.js +4 -3
- package/src/css/look-board.css +10 -2
- package/src/css/table.css +13 -0
- package/src/utils/builtInFormatters.js +22 -0
- package/src/utils/fillTemplate.js +7 -1
- package/src/utils/myTeam.js +24 -4
- package/src/utils/resolveFilterTokens.js +5 -4
- package/src/utils/transitionInputs.js +42 -1
package/l10n/en.json
CHANGED
|
@@ -2380,7 +2380,8 @@
|
|
|
2380
2380
|
"You save the order along with a view.": "You save the order along with a view.",
|
|
2381
2381
|
"me": "me",
|
|
2382
2382
|
"Open profile": "Open profile",
|
|
2383
|
-
"Link a contact from your address book.": "Link a contact from your address book."
|
|
2383
|
+
"Link a contact from your address book.": "Link a contact from your address book.",
|
|
2384
|
+
"The value could not be shown.": "The value could not be shown."
|
|
2384
2385
|
},
|
|
2385
2386
|
"plurals": {
|
|
2386
2387
|
"{count} member": [
|
package/l10n/nl.json
CHANGED
|
@@ -2630,7 +2630,11 @@
|
|
|
2630
2630
|
"You save the order along with a view.": "De sortering bewaart u mee in een weergave.",
|
|
2631
2631
|
"me": "ik",
|
|
2632
2632
|
"Open profile": "Profiel openen",
|
|
2633
|
-
"Link a contact from your address book.": "Koppel een contact uit je adresboek."
|
|
2633
|
+
"Link a contact from your address book.": "Koppel een contact uit je adresboek.",
|
|
2634
|
+
"The value could not be shown.": "De waarde kon niet worden getoond.",
|
|
2635
|
+
"Save as draft": "Opslaan als concept",
|
|
2636
|
+
"Draft saved at {time}": "Concept opgeslagen om {time}",
|
|
2637
|
+
"The draft could not be saved": "Het concept kon niet worden opgeslagen"
|
|
2634
2638
|
},
|
|
2635
2639
|
"plurals": {
|
|
2636
2640
|
"{count} member": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.140.0",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -770,9 +770,10 @@ import { DEFAULT_FORGE, resolveForge } from '../../utils/forge.js'
|
|
|
770
770
|
import { BUILT_IN_KB_PROVIDERS } from '../../utils/kbSearchProviders.js'
|
|
771
771
|
import { createManifestTranslate } from '../../utils/manifestTranslate.js'
|
|
772
772
|
import { installModalStack, uninstallModalStack } from '../../utils/modalStack.js'
|
|
773
|
+
import { loadMyTeam } from '../../utils/myTeam.js'
|
|
773
774
|
import { OBJECTS_CHANGED_EVENT } from '../../utils/objectSignals.js'
|
|
774
775
|
import { resolveBrand } from '../../utils/resolveBrand.js'
|
|
775
|
-
import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
776
|
+
import { dropOptionalUnresolved, MY_TEAM_TOKEN, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
776
777
|
import { pageOffersSavedViews } from '../../utils/savedViewHelpers.js'
|
|
777
778
|
import { normalizeSavedViewMenu, SAVED_VIEW_MENU_KEY, SAVED_VIEW_MENU_PREFIX, syncSavedViewMenu, toggleSavedViewMenuEntry, withSavedViewMenu } from '../../utils/savedViewMenu.js'
|
|
778
779
|
import { passesContextPredicates } from '../../utils/visibleIfContext.js'
|
|
@@ -4475,7 +4476,13 @@ export default {
|
|
|
4475
4476
|
async _fetchMenuItemCount(item) {
|
|
4476
4477
|
try {
|
|
4477
4478
|
const { register, schema, filter } = item.count
|
|
4478
|
-
const
|
|
4479
|
+
const raw = filter && typeof filter === 'object' ? filter : {}
|
|
4480
|
+
// `@myTeam` counts as the list filters: once the team is read,
|
|
4481
|
+
// every team the app answers, and without one no team at all.
|
|
4482
|
+
if (Object.values(raw).some((value) => value === MY_TEAM_TOKEN || (Array.isArray(value) && value.includes(MY_TEAM_TOKEN)))) {
|
|
4483
|
+
await loadMyTeam()
|
|
4484
|
+
}
|
|
4485
|
+
const resolved = dropOptionalUnresolved(resolveFilterTokens(raw, {}))
|
|
4479
4486
|
const url = generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}`)
|
|
4480
4487
|
const { data } = await axios.get(url, { params: { _limit: 1, ...resolved } })
|
|
4481
4488
|
const total = data?.total
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
<!-- Built-in "label" widget: the value's label (an enum's translated
|
|
57
57
|
label) as plain text, where a list wants words and no pill. -->
|
|
58
58
|
<template v-else-if="widget === 'label'">
|
|
59
|
-
<span v-if="hasValue" :title="rawTitle">{{
|
|
59
|
+
<span v-if="hasValue" :title="rawTitle">{{ shownLabel }}</span>
|
|
60
60
|
<span v-else class="cn-cell-renderer__dash">—</span>
|
|
61
61
|
</template>
|
|
62
62
|
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
shapes it; the colour stays keyed on the raw value. -->
|
|
66
66
|
<template v-else-if="widget === 'badge'">
|
|
67
67
|
<CnStatusBadge v-if="hasValue"
|
|
68
|
-
:label="
|
|
68
|
+
:label="shownLabel"
|
|
69
69
|
:variant="badgeVariant"
|
|
70
70
|
:colorMap="badgeColorMap"
|
|
71
71
|
:colorKey="badgeColorKey"
|
|
@@ -438,7 +438,7 @@ import { ageInDays, boardDateLocale, formatAge, formatBoardDate, formatRelativeD
|
|
|
438
438
|
import { daysUntil, parseDateValue, resolveDateVariant } from '../../utils/dateVariant.js'
|
|
439
439
|
import { pillLook } from '../../utils/detailActionModel.js'
|
|
440
440
|
import { resolveDue } from '../../utils/dueText.js'
|
|
441
|
-
import { formatBoardDateTime } from '../../utils/fillTemplate.js'
|
|
441
|
+
import { fillTemplate, formatBoardDateTime } from '../../utils/fillTemplate.js'
|
|
442
442
|
import { formatMoney } from '../../utils/formatMetric.js'
|
|
443
443
|
import { groupDisplayName, loadGroupDisplayName } from '../../utils/groupAutocomplete.js'
|
|
444
444
|
import { objectFieldValue } from '../../utils/objectName.js'
|
|
@@ -1455,6 +1455,22 @@ export default {
|
|
|
1455
1455
|
* @return {string}
|
|
1456
1456
|
* @spec openspec/changes/cell-labels-and-draft-indicator/specs/cell-labels-and-draft-indicator/spec.md#requirement-a-built-in-cell-widget-shows-an-enum-value-by-its-label
|
|
1457
1457
|
*/
|
|
1458
|
+
/**
|
|
1459
|
+
* What a `label` or `badge` cell shows: its label, worded by
|
|
1460
|
+
* `widgetProps.textTemplate` when given (translated, filled from the
|
|
1461
|
+
* row, `{value}` the label; DqPostvak "Lijkt op 2026-0079").
|
|
1462
|
+
*
|
|
1463
|
+
* @return {string}
|
|
1464
|
+
* @spec openspec/changes/cell-text-template/specs/cell-text-template/spec.md
|
|
1465
|
+
*/
|
|
1466
|
+
shownLabel() {
|
|
1467
|
+
const template = this.widgetProps && this.widgetProps.textTemplate
|
|
1468
|
+
if (typeof template !== 'string' || template === '') {
|
|
1469
|
+
return this.labelledValue
|
|
1470
|
+
}
|
|
1471
|
+
return fillTemplate(this.cnTranslate(template), { ...(this.row || {}), value: this.labelledValue })
|
|
1472
|
+
},
|
|
1473
|
+
|
|
1458
1474
|
labelledValue() {
|
|
1459
1475
|
if (!this.hasFormatter && this.isEnum && this.hasValue && typeof this.value !== 'object') {
|
|
1460
1476
|
return this.enumLabel
|
|
@@ -109,6 +109,7 @@ Fallback slot — shown when ApexCharts is not available:
|
|
|
109
109
|
| `barMax` | Number | `null` | `bar-list`: the value a full track stands for (100 for percentages); null takes the largest value |
|
|
110
110
|
| `matrixLabelWidth` | Number | `null` | `matrix`: the row-label column's width in px; a long label wraps (DqProcesanalyse "Dagen per fase": 240). Null sizes the column to its longest label, at least 120px |
|
|
111
111
|
| `footnote` | String | `''` | A line under the chart, through the host's translate: under the rows of a `bar-list` or `matrix`, under the plot of a `line`, `area` or `bar` chart |
|
|
112
|
+
| `notice` | Object | `null` | A toned note card under the chart: `{ lead?, text?, values?, labels?, tone? }`. `{name}` in `lead` (bold) and `text` is filled from `values[name]`, a literal or a record token (`@object.<field>`); `labels[name]` (`"rows"`, `"columns"`, `"values"` or a map) turns a key into words. `tone`: info, success, warning, error. Hidden while a value is missing |
|
|
112
113
|
| `referenceLines` | Array | `[]` | Dashed horizontal reference lines on the value axis of a line, area or bar chart, such as a norm: `[{ value, label?, color? }]`. The label goes through the host's translate; the colour defaults to the theme's error colour (board DqDoorlooptijd). `valuePath` reads the value from the endpoint answer instead, `{value}` in the label writes it, and a line without a value is not drawn. |
|
|
113
114
|
| `categoryFormat` | String | `''` | How date categories read on the x-axis: `month` ("jan") or `month-year` ("jan 2026"), in the user's language, or `week` for ISO week keys ("2026-W40" as "w40"). Categories that are not dates are left as is; a bare year key stays a year. |
|
|
114
115
|
| `emptyCellLabel` | String | `''` | `matrix`: the text of a cell without a value ("geen"), through the host's translate. Empty leaves the cell blank |
|
|
@@ -148,12 +148,25 @@
|
|
|
148
148
|
data-testid="cn-chart-widget-footnote">
|
|
149
149
|
{{ hostTranslate(footnote) }}
|
|
150
150
|
</p>
|
|
151
|
+
<!-- `notice`: a toned note card under the chart whose words carry
|
|
152
|
+
values from the page record (DqHandhaving "Advies: ..."). -->
|
|
153
|
+
<NcNoteCard
|
|
154
|
+
v-if="noticeParts"
|
|
155
|
+
:type="noticeParts.type"
|
|
156
|
+
:data-tone="noticeParts.type"
|
|
157
|
+
class="cn-chart-widget__notice"
|
|
158
|
+
data-testid="cn-chart-widget-notice">
|
|
159
|
+
<p class="cn-chart-widget__notice-text">
|
|
160
|
+
<strong v-if="noticeParts.lead">{{ noticeParts.lead }}</strong>{{ noticeParts.lead && noticeParts.text ? ' ' : '' }}{{ noticeParts.text }}
|
|
161
|
+
</p>
|
|
162
|
+
</NcNoteCard>
|
|
151
163
|
</div>
|
|
152
164
|
</template>
|
|
153
165
|
|
|
154
166
|
<script>
|
|
155
167
|
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
|
156
168
|
import { getLanguage, translate as t } from '@nextcloud/l10n'
|
|
169
|
+
import { NcNoteCard } from '@nextcloud/vue'
|
|
157
170
|
import { inject, markRaw, ref } from 'vue'
|
|
158
171
|
import VueApexCharts from 'vue3-apexcharts'
|
|
159
172
|
import { useDataSource } from '../../composables/useDataSource.js'
|
|
@@ -405,7 +418,7 @@ function formatterFor(vf) {
|
|
|
405
418
|
export default {
|
|
406
419
|
name: 'CnChartWidget',
|
|
407
420
|
|
|
408
|
-
components: { CnEndpointNotice },
|
|
421
|
+
components: { CnEndpointNotice, NcNoteCard },
|
|
409
422
|
|
|
410
423
|
inject: {
|
|
411
424
|
/**
|
|
@@ -467,6 +480,24 @@ export default {
|
|
|
467
480
|
default: null,
|
|
468
481
|
},
|
|
469
482
|
|
|
483
|
+
/**
|
|
484
|
+
* A toned note card under the chart whose words carry values from the
|
|
485
|
+
* page record: `{ lead?, text?, values?, labels?, tone? }`. `lead`
|
|
486
|
+
* (bold) and `text` go through the host's translate, then each
|
|
487
|
+
* `{name}` is filled from `values[name]`, a literal or a token such as
|
|
488
|
+
* `@object.recommendedIntervention`. `labels[name]` turns a key into
|
|
489
|
+
* words: `"rows"`, `"columns"` or `"values"` read the matrix's
|
|
490
|
+
* cellSource label maps, an object is its own map. `tone` is info
|
|
491
|
+
* (default), success, warning or error. Hidden while a value is
|
|
492
|
+
* missing, so a half-filled sentence never shows.
|
|
493
|
+
*
|
|
494
|
+
* @spec openspec/changes/chart-notice/specs/chart-notice/spec.md#requirement-a-chart-can-carry-a-notice-with-record-values
|
|
495
|
+
*/
|
|
496
|
+
notice: {
|
|
497
|
+
type: Object,
|
|
498
|
+
default: null,
|
|
499
|
+
},
|
|
500
|
+
|
|
470
501
|
/**
|
|
471
502
|
* A line under the chart ("Klik een balk om de zaken te zien"),
|
|
472
503
|
* through the host's translate: under the rows of a `bar-list` or
|
|
@@ -1601,6 +1632,38 @@ export default {
|
|
|
1601
1632
|
}
|
|
1602
1633
|
},
|
|
1603
1634
|
|
|
1635
|
+
/**
|
|
1636
|
+
* The notice's bold lead, text and note-card type, values filled in;
|
|
1637
|
+
* null when there is none or a value is missing.
|
|
1638
|
+
*
|
|
1639
|
+
* @return {{lead: string, text: string, type: string}|null} The notice.
|
|
1640
|
+
* @spec openspec/changes/chart-notice/specs/chart-notice/spec.md#requirement-a-chart-can-carry-a-notice-with-record-values
|
|
1641
|
+
*/
|
|
1642
|
+
noticeParts() {
|
|
1643
|
+
const def = this.notice
|
|
1644
|
+
if (!def || typeof def !== 'object' || (!def.lead && !def.text)) {
|
|
1645
|
+
return null
|
|
1646
|
+
}
|
|
1647
|
+
const declared = def.values && typeof def.values === 'object' ? def.values : {}
|
|
1648
|
+
const resolved = resolveFilterTokens(declared, this.chartTokenCtx())
|
|
1649
|
+
const source = this.cellSource && typeof this.cellSource === 'object' ? this.cellSource : {}
|
|
1650
|
+
const maps = { rows: source.rowLabels, columns: source.columnLabels, values: source.valueLabels }
|
|
1651
|
+
const words = {}
|
|
1652
|
+
for (const name of Object.keys(declared)) {
|
|
1653
|
+
const raw = resolved[name]
|
|
1654
|
+
if (raw === null || raw === undefined || raw === '' || (typeof raw === 'string' && raw.charAt(0) === '@')) {
|
|
1655
|
+
return null
|
|
1656
|
+
}
|
|
1657
|
+
const ref = def.labels && def.labels[name]
|
|
1658
|
+
const map = typeof ref === 'string' ? maps[ref] : ref
|
|
1659
|
+
const label = map && typeof map === 'object' && typeof map[String(raw)] === 'string' ? map[String(raw)] : null
|
|
1660
|
+
words[name] = label ? this.hostTranslate(label) : String(raw)
|
|
1661
|
+
}
|
|
1662
|
+
const fill = (key) => (key ? this.hostTranslate(key).replace(/\{(\w+)\}/g, (match, name) => (name in words ? words[name] : match)) : '')
|
|
1663
|
+
const type = ['info', 'success', 'warning', 'error'].includes(def.tone) ? def.tone : 'info'
|
|
1664
|
+
return { lead: fill(def.lead), text: fill(def.text), type }
|
|
1665
|
+
},
|
|
1666
|
+
|
|
1604
1667
|
/**
|
|
1605
1668
|
* The matrix cell of the record (`highlight: { row, column }`, tokens
|
|
1606
1669
|
* such as `@object.behaviour` resolved), as row and column indexes. A
|
|
@@ -2948,6 +3011,14 @@ export default {
|
|
|
2948
3011
|
margin: 0;
|
|
2949
3012
|
}
|
|
2950
3013
|
|
|
3014
|
+
.cn-chart-widget__notice {
|
|
3015
|
+
margin: 12px 0 0;
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
.cn-chart-widget__notice-text {
|
|
3019
|
+
margin: 0;
|
|
3020
|
+
}
|
|
3021
|
+
|
|
2951
3022
|
/* Under an ApexCharts plot the footnote is a sibling of the canvas, so it keeps
|
|
2952
3023
|
its own line height while the canvas takes the rest of a fitted column. */
|
|
2953
3024
|
.cn-chart-widget > .cn-chart-widget__footnote {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
<template>
|
|
7
7
|
<div class="cn-case-widget-card">
|
|
8
8
|
<CnConversationThread
|
|
9
|
+
ref="thread"
|
|
9
10
|
:messages="messages"
|
|
10
11
|
:title="tr(content.title || '')"
|
|
11
12
|
:subtitle="tr(content.subtitle || '')"
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
data-testid="cn-conversation-templates"
|
|
35
36
|
@apply="applyTemplate($event, setDraft)" />
|
|
36
37
|
</template>
|
|
37
|
-
<template v-if="replyOptions.length > 0" #reply-actions>
|
|
38
|
+
<template v-if="replyOptions.length > 0 || draftUrl" #reply-actions="{ draft }">
|
|
38
39
|
<NcCheckboxRadioSwitch
|
|
39
40
|
v-for="option in replyOptions"
|
|
40
41
|
:key="option.key"
|
|
@@ -42,6 +43,19 @@
|
|
|
42
43
|
:data-testid="`cn-conversation-option-${option.key}`">
|
|
43
44
|
{{ tr(option.label) }}
|
|
44
45
|
</NcCheckboxRadioSwitch>
|
|
46
|
+
<span v-if="draftUrl && draftStatus"
|
|
47
|
+
class="cn-conversation-widget__draft-status"
|
|
48
|
+
role="status"
|
|
49
|
+
data-testid="cn-conversation-draft-status">
|
|
50
|
+
{{ draftStatus }}
|
|
51
|
+
</span>
|
|
52
|
+
<NcButton
|
|
53
|
+
v-if="draftUrl"
|
|
54
|
+
data-testid="cn-conversation-save-draft"
|
|
55
|
+
:disabled="savingDraft"
|
|
56
|
+
@click="saveDraft(draft)">
|
|
57
|
+
{{ t('nextcloud-vue', 'Save as draft') }}
|
|
58
|
+
</NcButton>
|
|
45
59
|
</template>
|
|
46
60
|
<template v-if="headerAction || statusBadgeText" #header-actions>
|
|
47
61
|
<span v-if="statusBadgeText"
|
|
@@ -71,11 +85,13 @@
|
|
|
71
85
|
|
|
72
86
|
<script>
|
|
73
87
|
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
|
88
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
74
89
|
import { NcButton, NcCheckboxRadioSwitch } from '@nextcloud/vue'
|
|
75
90
|
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
76
91
|
import CnTemplateSelect from '../CnTemplateSelect/CnTemplateSelect.vue'
|
|
77
92
|
import CnConversationThread from './CnConversationThread.vue'
|
|
78
93
|
import { dispatchAction } from '../../utils/actionsDispatcher.js'
|
|
94
|
+
import { formatTimeOfDay } from '../../utils/boardDate.js'
|
|
79
95
|
import { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js'
|
|
80
96
|
import { readPath } from '../../utils/readPath.js'
|
|
81
97
|
import { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'
|
|
@@ -178,6 +194,10 @@ export default {
|
|
|
178
194
|
acting: '',
|
|
179
195
|
/** The threads `statusBadge` reads. */
|
|
180
196
|
badgeRows: [],
|
|
197
|
+
/** What the last draft save said: when it was saved, or that it failed. */
|
|
198
|
+
draftStatus: '',
|
|
199
|
+
/** Whether a draft is being saved. */
|
|
200
|
+
savingDraft: false,
|
|
181
201
|
}
|
|
182
202
|
},
|
|
183
203
|
|
|
@@ -315,6 +335,21 @@ export default {
|
|
|
315
335
|
* @return {string} The URL, or '' when the content names none.
|
|
316
336
|
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
|
|
317
337
|
*/
|
|
338
|
+
/**
|
|
339
|
+
* The reply draft's url (`content.draft.url`, `@objectId` filled in),
|
|
340
|
+
* or '' without one.
|
|
341
|
+
*
|
|
342
|
+
* @return {string}
|
|
343
|
+
* @spec openspec/changes/conversation-reply-draft/specs/conversation-reply-draft/spec.md#requirement-a-reply-can-be-saved-as-a-draft-and-comes-back
|
|
344
|
+
*/
|
|
345
|
+
draftUrl() {
|
|
346
|
+
const url = this.content.draft && this.content.draft.url
|
|
347
|
+
if (typeof url !== 'string' || url === '') {
|
|
348
|
+
return ''
|
|
349
|
+
}
|
|
350
|
+
return url.replace(/@objectId/g, encodeURIComponent(String(this.objectId ?? '')))
|
|
351
|
+
},
|
|
352
|
+
|
|
318
353
|
endpoint() {
|
|
319
354
|
const url = this.content.endpoint
|
|
320
355
|
if (typeof url !== 'string' || url === '') {
|
|
@@ -340,19 +375,101 @@ export default {
|
|
|
340
375
|
handler() {
|
|
341
376
|
this.loadSource()
|
|
342
377
|
this.loadBadge()
|
|
378
|
+
this.loadDraft()
|
|
343
379
|
},
|
|
344
380
|
},
|
|
345
381
|
},
|
|
346
382
|
|
|
347
383
|
mounted() {
|
|
348
384
|
subscribe('cn:page:refresh', this.onPageRefresh)
|
|
385
|
+
subscribe('cn:conversation:reply-sent', this.onReplySentElsewhere)
|
|
349
386
|
},
|
|
350
387
|
|
|
351
388
|
beforeUnmount() {
|
|
352
389
|
unsubscribe('cn:page:refresh', this.onPageRefresh)
|
|
390
|
+
unsubscribe('cn:conversation:reply-sent', this.onReplySentElsewhere)
|
|
353
391
|
},
|
|
354
392
|
|
|
355
393
|
methods: {
|
|
394
|
+
t,
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Read the saved reply draft into the reply box.
|
|
398
|
+
*
|
|
399
|
+
* @return {Promise<void>}
|
|
400
|
+
* @spec openspec/changes/conversation-reply-draft/specs/conversation-reply-draft/spec.md#requirement-a-reply-can-be-saved-as-a-draft-and-comes-back
|
|
401
|
+
*/
|
|
402
|
+
async loadDraft() {
|
|
403
|
+
if (!this.draftUrl) {
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const { default: axios } = await import('@nextcloud/axios')
|
|
408
|
+
const { generateUrl } = await import('@nextcloud/router')
|
|
409
|
+
const { data } = await axios.get(generateUrl(this.draftUrl))
|
|
410
|
+
const text = data && typeof data.text === 'string' ? data.text : ''
|
|
411
|
+
if (text !== '' && this.$refs.thread) {
|
|
412
|
+
this.$refs.thread.setDraft(text)
|
|
413
|
+
}
|
|
414
|
+
} catch {
|
|
415
|
+
// No saved draft to read leaves the box empty.
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Store the reply box as the draft and say when it was saved.
|
|
421
|
+
*
|
|
422
|
+
* @param {string} text The reply box's text.
|
|
423
|
+
* @return {Promise<void>}
|
|
424
|
+
*/
|
|
425
|
+
async saveDraft(text) {
|
|
426
|
+
this.savingDraft = true
|
|
427
|
+
try {
|
|
428
|
+
await this.storeDraft(String(text ?? ''))
|
|
429
|
+
this.draftStatus = t('nextcloud-vue', 'Draft saved at {time}', { time: formatTimeOfDay(new Date()) })
|
|
430
|
+
} catch {
|
|
431
|
+
this.draftStatus = t('nextcloud-vue', 'The draft could not be saved')
|
|
432
|
+
} finally {
|
|
433
|
+
this.savingDraft = false
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* A reply on this record went out through a dialog or another surface
|
|
439
|
+
* (`cn:conversation:reply-sent` with `{ objectId }`): the saved draft
|
|
440
|
+
* and the reply box are cleared.
|
|
441
|
+
*
|
|
442
|
+
* @param {{objectId?: string}} payload The record the reply was on.
|
|
443
|
+
* @return {Promise<void>}
|
|
444
|
+
* @spec openspec/changes/conversation-reply-draft/specs/conversation-reply-draft/spec.md#requirement-a-reply-can-be-saved-as-a-draft-and-comes-back
|
|
445
|
+
*/
|
|
446
|
+
async onReplySentElsewhere(payload) {
|
|
447
|
+
if (!this.draftUrl || String(payload?.objectId ?? '') !== String(this.objectId ?? '')) {
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
if (this.$refs.thread) {
|
|
451
|
+
this.$refs.thread.setDraft('')
|
|
452
|
+
}
|
|
453
|
+
this.draftStatus = ''
|
|
454
|
+
try {
|
|
455
|
+
await this.storeDraft('')
|
|
456
|
+
} catch {
|
|
457
|
+
// The next save overwrites a draft that could not be cleared.
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* PUT the draft text.
|
|
463
|
+
*
|
|
464
|
+
* @param {string} text The text; '' clears the draft.
|
|
465
|
+
* @return {Promise<void>}
|
|
466
|
+
*/
|
|
467
|
+
async storeDraft(text) {
|
|
468
|
+
const { default: axios } = await import('@nextcloud/axios')
|
|
469
|
+
const { generateUrl } = await import('@nextcloud/router')
|
|
470
|
+
await axios.put(generateUrl(this.draftUrl), { text })
|
|
471
|
+
},
|
|
472
|
+
|
|
356
473
|
/**
|
|
357
474
|
* Read the messages again when the page refreshes (a dialog sent one).
|
|
358
475
|
*
|
|
@@ -691,6 +808,9 @@ export default {
|
|
|
691
808
|
if (typeof text !== 'string' || text === '') {
|
|
692
809
|
return
|
|
693
810
|
}
|
|
811
|
+
if (this.draftUrl) {
|
|
812
|
+
this.storeDraft('')
|
|
813
|
+
}
|
|
694
814
|
this.sentHere.push({
|
|
695
815
|
id: `sent-${this.sentHere.length}`,
|
|
696
816
|
time: new Date().toISOString(),
|
|
@@ -722,4 +842,9 @@ export default {
|
|
|
722
842
|
border-radius: var(--border-radius-large);
|
|
723
843
|
padding: calc(5 * var(--default-grid-baseline));
|
|
724
844
|
}
|
|
845
|
+
|
|
846
|
+
.cn-conversation-widget__draft-status {
|
|
847
|
+
color: var(--color-text-maxcontrast);
|
|
848
|
+
font-size: 13px;
|
|
849
|
+
}
|
|
725
850
|
</style>
|
|
@@ -66,19 +66,25 @@
|
|
|
66
66
|
v-else-if="row.to"
|
|
67
67
|
:to="row.to"
|
|
68
68
|
class="cn-facts-widget__value-link">
|
|
69
|
-
{{ row
|
|
69
|
+
{{ shownValue(row) }}
|
|
70
70
|
</router-link>
|
|
71
|
-
<span v-else>{{ row
|
|
71
|
+
<span v-else>{{ shownValue(row) }}</span>
|
|
72
72
|
<button
|
|
73
73
|
v-if="row.reveal && !revealed[row.key]"
|
|
74
74
|
type="button"
|
|
75
75
|
class="cn-facts-widget__inline-button"
|
|
76
76
|
:aria-label="revealLabel(row)"
|
|
77
77
|
:data-testid="`cn-fact-reveal-${row.key}`"
|
|
78
|
+
:disabled="revealing === row.key"
|
|
78
79
|
@click="reveal(row)">
|
|
79
80
|
<EyeOutline :size="16" aria-hidden="true" />
|
|
80
81
|
{{ showLabel }}
|
|
81
82
|
</button>
|
|
83
|
+
<span
|
|
84
|
+
v-if="revealErrors[row.key]"
|
|
85
|
+
class="cn-facts-widget__reveal-error"
|
|
86
|
+
role="alert"
|
|
87
|
+
:data-testid="`cn-fact-reveal-error-${row.key}`">{{ revealErrors[row.key] }}</span>
|
|
82
88
|
<button
|
|
83
89
|
v-if="row.copy"
|
|
84
90
|
type="button"
|
|
@@ -420,7 +426,7 @@ export default {
|
|
|
420
426
|
* `as` is `row` (default when `label` or `icon` is set), `title`, `line`, `text` (a line in the normal ink), `lead` (an icon tile or initials beside `title` and `subtitle`), `pill` or `meter`.
|
|
421
427
|
* `countFrom` (`{ register, schema, filter? }`, `@objectId` / `@object.<field>` in the filter) shows how many records that list holds ("Lopende zaken 43"); the row is left out while the count cannot be read.
|
|
422
428
|
* A `meter` shows the amount at `path` against the limit at `limitPath` (or a constant `limit`) as "12 of 50" with a bar; past the limit the bar turns red and the text says "over the limit"; without a positive limit it shows the amount and no bar.
|
|
423
|
-
* A row's `format` may also be `email`, `phone` or `url` (a link) or `masked` (the last four characters; `reveal: true` adds Show). `copy: true` adds a copy button; `icon` replaces the label.
|
|
429
|
+
* A row's `format` may also be `email`, `phone` or `url` (a link) or `masked` (the last four characters; `reveal: true` adds Show, which unmasks the record's value; `reveal: { url, path? }` makes Show ask the server for the value at `url` (tokens resolved), read at `path` (else the entry's path), so a server that answers the mask can check and record the reveal). `copy: true` adds a copy button; `icon` replaces the label.
|
|
424
430
|
* A part may be `{ text }`, a constant; a `line` or `text` entry without `path` or `parts` shows its `text`.
|
|
425
431
|
* `resolve: { register, schema, labelField? }` on an entry or a part turns a value that is a reference (an id, or a list of ids) into the label of the referenced object (`labelField`, else `title`, then `name`): the case type or status of a task shows its name, not its uuid. The ids are fetched once per register and schema through the object store and shared by every facts widget on the page; the entry stays empty until its label is known, and when no label can be read. A resolved value works as a pill too: `map` is applied to the resolved label, and `variantMap` / `colorMap` look the tone up by the value of `resolve.toneField` on the referenced object when that is set (a status named by its `name`, coloured by its `state`), then by the resolved label, then by the stored value.
|
|
426
432
|
* `route` (`{ name, params?, query? }`, `@source.<path>` in a param) on a `title` or a `row` links its value to that route; a param that does not fill leaves it plain.
|
|
@@ -474,6 +480,9 @@ export default {
|
|
|
474
480
|
error: false,
|
|
475
481
|
/** Keys of the masked rows the reader chose to show. */
|
|
476
482
|
revealed: {},
|
|
483
|
+
revealedValues: {},
|
|
484
|
+
revealErrors: {},
|
|
485
|
+
revealing: '',
|
|
477
486
|
/** Key of the row whose value was just copied. */
|
|
478
487
|
copied: '',
|
|
479
488
|
/** Labels of the referenced objects read so far, by `refKey`. */
|
|
@@ -1321,7 +1330,12 @@ export default {
|
|
|
1321
1330
|
extras.external = true
|
|
1322
1331
|
} else if (field.format === 'masked') {
|
|
1323
1332
|
extras.masked = value.length > 4 ? '*'.repeat(Math.min(value.length - 4, 5)) + value.slice(-4) : value
|
|
1324
|
-
|
|
1333
|
+
const server = field.reveal && typeof field.reveal === 'object' && typeof field.reveal.url === 'string' && field.reveal.url !== ''
|
|
1334
|
+
extras.reveal = (field.reveal === true || server) && value.length > 4
|
|
1335
|
+
if (server) {
|
|
1336
|
+
extras.revealUrl = field.reveal.url
|
|
1337
|
+
extras.revealPath = typeof field.reveal.path === 'string' && field.reveal.path !== '' ? field.reveal.path : (field.path || '')
|
|
1338
|
+
}
|
|
1325
1339
|
}
|
|
1326
1340
|
return extras
|
|
1327
1341
|
},
|
|
@@ -1333,7 +1347,14 @@ export default {
|
|
|
1333
1347
|
* @param {object} row The row.
|
|
1334
1348
|
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead
|
|
1335
1349
|
*/
|
|
1336
|
-
reveal(row) {
|
|
1350
|
+
async reveal(row) {
|
|
1351
|
+
if (row.revealUrl) {
|
|
1352
|
+
const value = await this.fetchRevealed(row)
|
|
1353
|
+
if (value === null) {
|
|
1354
|
+
return
|
|
1355
|
+
}
|
|
1356
|
+
this.revealedValues = { ...this.revealedValues, [row.key]: value }
|
|
1357
|
+
}
|
|
1337
1358
|
this.revealed = { ...this.revealed, [row.key]: true }
|
|
1338
1359
|
const detail = { path: row.path }
|
|
1339
1360
|
/**
|
|
@@ -1344,6 +1365,47 @@ export default {
|
|
|
1344
1365
|
emit(FACTS_REVEAL_CHANNEL, { ...detail, objectId: this.tokenCtx.objectId, register: this.tokenCtx.register || null, schema: this.tokenCtx.schema || null })
|
|
1345
1366
|
},
|
|
1346
1367
|
|
|
1368
|
+
/**
|
|
1369
|
+
* The text a row shows: its mask until revealed, then the value the
|
|
1370
|
+
* server answered (a server reveal) or the value the record holds.
|
|
1371
|
+
*
|
|
1372
|
+
* @param {object} row The row.
|
|
1373
|
+
* @return {string}
|
|
1374
|
+
* @spec openspec/changes/facts-reveal-from-server/specs/facts-widget/spec.md#requirement-a-masked-fact-can-be-revealed-by-the-server
|
|
1375
|
+
*/
|
|
1376
|
+
shownValue(row) {
|
|
1377
|
+
if (!row.masked || !this.revealed[row.key]) {
|
|
1378
|
+
return row.masked || row.value
|
|
1379
|
+
}
|
|
1380
|
+
return Object.hasOwn(this.revealedValues, row.key) ? this.revealedValues[row.key] : row.value
|
|
1381
|
+
},
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Ask the server for a masked value (`reveal.url`, tokens resolved,
|
|
1385
|
+
* read at `reveal.path`), never from a cache: each show is a request
|
|
1386
|
+
* the server can check and record.
|
|
1387
|
+
*
|
|
1388
|
+
* @param {object} row The row.
|
|
1389
|
+
* @return {Promise<string|null>} The value, or null when it could not be read.
|
|
1390
|
+
* @spec openspec/changes/facts-reveal-from-server/specs/facts-widget/spec.md#requirement-a-masked-fact-can-be-revealed-by-the-server
|
|
1391
|
+
*/
|
|
1392
|
+
async fetchRevealed(row) {
|
|
1393
|
+
this.revealing = row.key
|
|
1394
|
+
this.revealErrors = { ...this.revealErrors, [row.key]: '' }
|
|
1395
|
+
try {
|
|
1396
|
+
const payload = await fetchEndpointSource({ url: row.revealUrl, responsePath: row.revealPath }, this.tokenCtx, { force: true })
|
|
1397
|
+
if (payload === null || payload === undefined || payload === '' || typeof payload === 'object') {
|
|
1398
|
+
throw new Error('no value')
|
|
1399
|
+
}
|
|
1400
|
+
return String(payload)
|
|
1401
|
+
} catch {
|
|
1402
|
+
this.revealErrors = { ...this.revealErrors, [row.key]: t('nextcloud-vue', 'The value could not be shown.') }
|
|
1403
|
+
return null
|
|
1404
|
+
} finally {
|
|
1405
|
+
this.revealing = ''
|
|
1406
|
+
}
|
|
1407
|
+
},
|
|
1408
|
+
|
|
1347
1409
|
/**
|
|
1348
1410
|
* Copy a row's value to the clipboard.
|
|
1349
1411
|
*
|
|
@@ -1955,6 +2017,11 @@ export default {
|
|
|
1955
2017
|
font: inherit;
|
|
1956
2018
|
}
|
|
1957
2019
|
|
|
2020
|
+
.cn-facts-widget__reveal-error {
|
|
2021
|
+
font-size: 13px;
|
|
2022
|
+
color: var(--color-text-error, var(--color-error));
|
|
2023
|
+
}
|
|
2024
|
+
|
|
1958
2025
|
.cn-facts-widget__inline-button {
|
|
1959
2026
|
background: var(--color-main-background);
|
|
1960
2027
|
border: 1px solid var(--color-border-dark);
|
|
@@ -775,6 +775,7 @@ import CnIcon from '../CnIcon/CnIcon.vue'
|
|
|
775
775
|
import { readUserPreference, writeUserPreference } from '../../composables/useUserPreferences.js'
|
|
776
776
|
import { dispatchAction } from '../../utils/actionsDispatcher.js'
|
|
777
777
|
import { fillTemplate } from '../../utils/fillTemplate.js'
|
|
778
|
+
import { readPath } from '../../utils/readPath.js'
|
|
778
779
|
import { isNewTabClick, isRowMiddleClick, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
|
|
779
780
|
import { ACTIONS_NEEDING_THE_FILES_PAGE, crumbsFor, hostActionApplies, joinPath } from './filesBrowser.js'
|
|
780
781
|
import {
|
|
@@ -3049,7 +3050,7 @@ export default {
|
|
|
3049
3050
|
? { path: this.currentPath }
|
|
3050
3051
|
: { fileId: node.fileid, fileName: node.basename, path: node.path }
|
|
3051
3052
|
if (type === 'open-modal') {
|
|
3052
|
-
wrapped = { ...action, props: { ...(action.props || {}), ...context } }
|
|
3053
|
+
wrapped = { ...action, props: { ...(action.props || {}), ...context, ...this.rowPropsOf(action, node) } }
|
|
3053
3054
|
} else if (type === 'handler') {
|
|
3054
3055
|
wrapped = { ...action, args: [...(action.args || []), node ?? this.folder] }
|
|
3055
3056
|
}
|
|
@@ -3060,6 +3061,32 @@ export default {
|
|
|
3060
3061
|
dispatchAction(wrapped, { router: this.$router || null })
|
|
3061
3062
|
},
|
|
3062
3063
|
|
|
3064
|
+
/**
|
|
3065
|
+
* The clicked file's row-data fields an action asks for as modal props
|
|
3066
|
+
* (`rowProps: { prop: field }`), or none.
|
|
3067
|
+
*
|
|
3068
|
+
* @param {object} action The declared action.
|
|
3069
|
+
* @param {object|null} node The file, or null for a New-menu entry.
|
|
3070
|
+
* @return {object} The props.
|
|
3071
|
+
* @spec openspec/changes/files-review-row-when/specs/files-review-row-when/spec.md#requirement-a-modal-action-can-carry-fields-of-the-clicked-files-row-data
|
|
3072
|
+
*/
|
|
3073
|
+
rowPropsOf(action, node) {
|
|
3074
|
+
const wanted = action && action.rowProps
|
|
3075
|
+
if (node === null || !wanted || typeof wanted !== 'object') {
|
|
3076
|
+
return {}
|
|
3077
|
+
}
|
|
3078
|
+
const byId = this.resolvedRowData && typeof this.resolvedRowData === 'object' ? this.resolvedRowData : {}
|
|
3079
|
+
const entry = byId[node.fileid] ?? byId[String(node.fileid)] ?? {}
|
|
3080
|
+
const out = {}
|
|
3081
|
+
for (const [prop, field] of Object.entries(wanted)) {
|
|
3082
|
+
const value = readPath(entry, String(field))
|
|
3083
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
3084
|
+
out[prop] = value
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
return out
|
|
3088
|
+
},
|
|
3089
|
+
|
|
3063
3090
|
/**
|
|
3064
3091
|
* A click on a linked row opens the file where it lives.
|
|
3065
3092
|
*
|
|
@@ -285,13 +285,19 @@ export function indexRowData(data, { path = '', key = 'fileId' } = {}) {
|
|
|
285
285
|
* `visibleIf.extension` lists file extensions (no case, a leading dot is
|
|
286
286
|
* allowed); `visibleIf.mime` lists mime types, where one ending in `/` or
|
|
287
287
|
* `/*` names the whole family. Every condition named must hold. An action
|
|
288
|
-
* without `visibleIf` is offered on every file, as before.
|
|
288
|
+
* without `visibleIf` is offered on every file, as before. An action with
|
|
289
|
+
* `when` is offered only while `when` is `true`.
|
|
289
290
|
*
|
|
290
291
|
* @param {object} action The declared action.
|
|
291
292
|
* @param {object} node The file (`basename`, `mime`).
|
|
292
293
|
* @return {boolean} True when the action belongs on this file.
|
|
293
294
|
*/
|
|
294
295
|
export function hostActionApplies(action, node) {
|
|
296
|
+
// `when`: the action applies only while it is true (a case type flag).
|
|
297
|
+
// @spec openspec/changes/files-review-row-when/specs/files-review-row-when/spec.md#requirement-a-files-browser-entry-can-apply-only-when-a-condition-holds
|
|
298
|
+
if (action && typeof action === 'object' && 'when' in action && action.when !== true) {
|
|
299
|
+
return false
|
|
300
|
+
}
|
|
295
301
|
const condition = action && action.visibleIf
|
|
296
302
|
if (!condition || typeof condition !== 'object') {
|
|
297
303
|
return true
|
|
@@ -78,6 +78,11 @@ export function normaliseFileColumns(columns) {
|
|
|
78
78
|
if (!entry || typeof entry !== 'object' || typeof entry.key !== 'string' || entry.key === '') {
|
|
79
79
|
return
|
|
80
80
|
}
|
|
81
|
+
// `when`: the column applies only while it is true (a case type flag).
|
|
82
|
+
// @spec openspec/changes/files-review-row-when/specs/files-review-row-when/spec.md#requirement-a-files-browser-entry-can-apply-only-when-a-condition-holds
|
|
83
|
+
if ('when' in entry && entry.when !== true) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
81
86
|
if (isBuiltInFileColumn(entry.key) && entry.source === undefined) {
|
|
82
87
|
out.push({ ...BUILT_IN_FILE_COLUMNS[entry.key], ...entry, builtIn: true })
|
|
83
88
|
return
|