@conduction/nextcloud-vue 2.139.0 → 2.141.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/CnAssignDialog/CnAssignDialog.vue.js +1 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +10 -2
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +28 -4
- 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 +64 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +76 -37
- 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/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -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/CnDataTable/CnDataTable.vue2.js +5 -3
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -0
- package/dist/esm/components/CnDataTable/CnDataTable.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/CnIndexSidebar/CnIndexSidebar.vue2.js +65 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +120 -69
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +47 -9
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.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/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +3 -3
- 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 +2 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +20 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js +34 -26
- 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 +3 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +6 -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 +961 -605
- 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/groupSublines.js +120 -0
- package/dist/esm/utils/groupSublines.js.map +1 -0
- 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 +1731 -846
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +961 -605
- package/l10n/en.json +3 -1
- package/l10n/nl.json +6 -1
- package/package.json +1 -1
- package/src/components/CnAppRoot/CnAppRoot.vue +9 -2
- package/src/components/CnAssignDialog/CnAssignDialog.vue +10 -2
- package/src/components/CnCellRenderer/CnCellRenderer.vue +30 -6
- package/src/components/CnChartWidget/CnChartWidget.md +2 -0
- package/src/components/CnChartWidget/CnChartWidget.vue +121 -5
- package/src/components/CnConversationThread/CnConversationWidget.vue +126 -1
- package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +6 -3
- 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/CnIndexSidebar/CnIndexSidebar.vue +101 -3
- package/src/components/CnMapWidget/CnMapWidget.vue +47 -9
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +28 -3
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +3 -3
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +20 -1
- package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +43 -9
- 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/dialog.css +13 -1
- package/src/css/look-board-detail.css +12 -9
- package/src/css/look-board-index.css +90 -7
- package/src/css/look-board.css +102 -2
- package/src/css/table.css +25 -0
- package/src/utils/builtInFormatters.js +22 -0
- package/src/utils/fillTemplate.js +7 -1
- package/src/utils/groupSublines.js +125 -0
- package/src/utils/myTeam.js +24 -4
- package/src/utils/resolveFilterTokens.js +5 -4
- package/src/utils/transitionInputs.js +42 -1
|
@@ -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
|
|
@@ -4670,6 +4670,8 @@ export default {
|
|
|
4670
4670
|
savedViews: [],
|
|
4671
4671
|
savedViewsLoading: false,
|
|
4672
4672
|
showSaveViewDialog: false,
|
|
4673
|
+
/** Whether the reader changed the view since it was opened or saved. */
|
|
4674
|
+
viewChanged: false,
|
|
4673
4675
|
viewPendingDelete: null,
|
|
4674
4676
|
/** An `api-call` header action waiting on its confirmation, or null. */
|
|
4675
4677
|
headerApiCallPending: null,
|
|
@@ -8706,12 +8708,20 @@ export default {
|
|
|
8706
8708
|
|
|
8707
8709
|
/**
|
|
8708
8710
|
* Whether the toolbar draws Save view: not when the page switches it
|
|
8709
|
-
* off (`savedViewSave: false`) or its header carries one
|
|
8711
|
+
* off (`savedViewSave: false`) or its header carries one, and in the
|
|
8712
|
+
* board look only once the view has changed.
|
|
8710
8713
|
*
|
|
8714
|
+
* @spec openspec/changes/board-save-view-only-when-changed/specs/saved-views/spec.md#requirement-the-board-look-offers-save-view-once-the-view-has-changed
|
|
8711
8715
|
* @return {boolean}
|
|
8712
8716
|
*/
|
|
8713
8717
|
toolbarSavesView() {
|
|
8714
|
-
|
|
8718
|
+
if (this.savedViewSave === false || this.headerSavesView) {
|
|
8719
|
+
return false
|
|
8720
|
+
}
|
|
8721
|
+
// The board look offers it once the view differs from the one
|
|
8722
|
+
// opened: changed by the reader, or, with no saved view open,
|
|
8723
|
+
// filtered by the address.
|
|
8724
|
+
return !this.isBoardLook || this.viewChanged || (!this.appliedSavedViewId && this.activeFilterChips.length > 0)
|
|
8715
8725
|
},
|
|
8716
8726
|
|
|
8717
8727
|
/**
|
|
@@ -10849,6 +10859,7 @@ export default {
|
|
|
10849
10859
|
* @return {void}
|
|
10850
10860
|
*/
|
|
10851
10861
|
onSearchEvent(value) {
|
|
10862
|
+
this.viewChanged = true
|
|
10852
10863
|
if (this.namedSourceSearches) {
|
|
10853
10864
|
this.namedSearchTerm = value || ''
|
|
10854
10865
|
}
|
|
@@ -10868,6 +10879,7 @@ export default {
|
|
|
10868
10879
|
* @return {void}
|
|
10869
10880
|
*/
|
|
10870
10881
|
onQuickFilterChange(index) {
|
|
10882
|
+
this.viewChanged = true
|
|
10871
10883
|
// `activeQuickFilterIndex` is a setup-returned ref; the
|
|
10872
10884
|
// Vue 2 ref-unwrap proxy makes plain assignment work.
|
|
10873
10885
|
this.activeQuickFilterIndex = index
|
|
@@ -10884,6 +10896,7 @@ export default {
|
|
|
10884
10896
|
* @return {void}
|
|
10885
10897
|
*/
|
|
10886
10898
|
onQuickFilterMultiChange(indices) {
|
|
10899
|
+
this.viewChanged = true
|
|
10887
10900
|
this.selectedQuickFilterIndices = Array.isArray(indices) ? indices : []
|
|
10888
10901
|
this.$emit('quick-filter-change', this.selectedQuickFilterIndices)
|
|
10889
10902
|
},
|
|
@@ -10899,6 +10912,7 @@ export default {
|
|
|
10899
10912
|
* @return {void}
|
|
10900
10913
|
*/
|
|
10901
10914
|
onSidebarSortChange(keys, value) {
|
|
10915
|
+
this.viewChanged = true
|
|
10902
10916
|
if (value !== undefined && this.selectSortOption(value)) {
|
|
10903
10917
|
return
|
|
10904
10918
|
}
|
|
@@ -10978,6 +10992,7 @@ export default {
|
|
|
10978
10992
|
* @return {void}
|
|
10979
10993
|
*/
|
|
10980
10994
|
onSortEvent(payload) {
|
|
10995
|
+
this.viewChanged = true
|
|
10981
10996
|
// An order by keys (a column header, a field in the Sort section)
|
|
10982
10997
|
// leaves an endpoint-ranked order.
|
|
10983
10998
|
if (this.sortSource.value) {
|
|
@@ -11181,6 +11196,7 @@ export default {
|
|
|
11181
11196
|
* @return {void}
|
|
11182
11197
|
*/
|
|
11183
11198
|
onClearFilters() {
|
|
11199
|
+
this.viewChanged = true
|
|
11184
11200
|
// "Clear all" on the Active line also undoes the quick filters it
|
|
11185
11201
|
// listed (a tab without an `activeLabel` is not a filter there).
|
|
11186
11202
|
for (const chip of this.quickFilterActiveChips) {
|
|
@@ -11289,6 +11305,7 @@ export default {
|
|
|
11289
11305
|
* @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-a-board-can-switch-the-field-its-columns-come-from
|
|
11290
11306
|
*/
|
|
11291
11307
|
onBoardColumnFieldChange(field) {
|
|
11308
|
+
this.viewChanged = true
|
|
11292
11309
|
this.boardColumnFieldChoice = field
|
|
11293
11310
|
/**
|
|
11294
11311
|
* @event board-column-field-change The board's column switch picked another field. Payload: the field name.
|
|
@@ -11304,6 +11321,7 @@ export default {
|
|
|
11304
11321
|
* @return {void}
|
|
11305
11322
|
*/
|
|
11306
11323
|
onBoardNarrowChange(value) {
|
|
11324
|
+
this.viewChanged = true
|
|
11307
11325
|
this.onFilterEvent({ key: this.boardNarrow.field, values: value === '' ? [] : [value] })
|
|
11308
11326
|
},
|
|
11309
11327
|
|
|
@@ -11366,6 +11384,7 @@ export default {
|
|
|
11366
11384
|
* @return {void}
|
|
11367
11385
|
*/
|
|
11368
11386
|
onFilterEvent(payload) {
|
|
11387
|
+
this.viewChanged = true
|
|
11369
11388
|
if (this.isSelfFetchMode && typeof this.list.onFilterChange === 'function') {
|
|
11370
11389
|
this.list.onFilterChange(payload.key, payload.values)
|
|
11371
11390
|
this.persistViewStateToRoute(this.currentViewState())
|
|
@@ -11388,6 +11407,7 @@ export default {
|
|
|
11388
11407
|
* @spec openspec/changes/screens-index-list-parity/specs/index-list-board-look/spec.md#requirement-the-toolbar-sits-on-the-ground-in-two-rows
|
|
11389
11408
|
*/
|
|
11390
11409
|
onRemoveActiveFilter(chip) {
|
|
11410
|
+
this.viewChanged = true
|
|
11391
11411
|
if (chip && chip.searchTerm === true) {
|
|
11392
11412
|
this.onSearchEvent('')
|
|
11393
11413
|
return
|
|
@@ -12356,6 +12376,7 @@ export default {
|
|
|
12356
12376
|
* @return {void}
|
|
12357
12377
|
*/
|
|
12358
12378
|
onColumnsEvent(columns) {
|
|
12379
|
+
this.viewChanged = true
|
|
12359
12380
|
if (this.isSelfFetchMode && this.list.visibleColumns) {
|
|
12360
12381
|
this.list.visibleColumns.value = columns
|
|
12361
12382
|
}
|
|
@@ -12403,6 +12424,7 @@ export default {
|
|
|
12403
12424
|
* @return {void}
|
|
12404
12425
|
*/
|
|
12405
12426
|
onColumnsReorder(columns) {
|
|
12427
|
+
this.viewChanged = true
|
|
12406
12428
|
this.onColumnsEvent(columns)
|
|
12407
12429
|
},
|
|
12408
12430
|
|
|
@@ -12413,6 +12435,7 @@ export default {
|
|
|
12413
12435
|
* @return {void}
|
|
12414
12436
|
*/
|
|
12415
12437
|
onPinChange(count) {
|
|
12438
|
+
this.viewChanged = true
|
|
12416
12439
|
if (!this.personalColumns || Array.isArray(this.appliedViewColumns)) {
|
|
12417
12440
|
return
|
|
12418
12441
|
}
|
|
@@ -13316,6 +13339,10 @@ export default {
|
|
|
13316
13339
|
loadMyTeam().then(() => this.onApplySavedView(view, { keepSavedViewKey }))
|
|
13317
13340
|
return
|
|
13318
13341
|
}
|
|
13342
|
+
// An opened view is unchanged until the reader changes it.
|
|
13343
|
+
this.$nextTick(() => {
|
|
13344
|
+
this.viewChanged = false
|
|
13345
|
+
})
|
|
13319
13346
|
const ctx = typeof this.selfFetchTokenCtx === 'function' ? this.selfFetchTokenCtx() : {}
|
|
13320
13347
|
state.filters = dropOptionalUnresolved(resolveFilterTokens(state.filters, ctx))
|
|
13321
13348
|
this.appliedViewFiltersKey = this.viewFiltersKey(state.filters)
|
|
@@ -13441,6 +13468,7 @@ export default {
|
|
|
13441
13468
|
}
|
|
13442
13469
|
this.savedViews = [...this.savedViews, view]
|
|
13443
13470
|
this.showSaveViewDialog = false
|
|
13471
|
+
this.viewChanged = false
|
|
13444
13472
|
this.sidebarActiveTab = 'filters-tab'
|
|
13445
13473
|
this.sidebarOpen = true
|
|
13446
13474
|
this.toastSavedView('success', t('nextcloud-vue', 'View "{name}" saved', { name }))
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
</p>
|
|
104
104
|
</div>
|
|
105
105
|
|
|
106
|
-
<div v-if="hasActiveFilters" class="cn-index-sidebar__section">
|
|
106
|
+
<div v-if="hasActiveFilters && !isBoardLook" class="cn-index-sidebar__section">
|
|
107
107
|
<NcButton variant="tertiary" @click="onClearAll">
|
|
108
108
|
<template #icon>
|
|
109
109
|
<FilterRemoveOutline :size="20" />
|
|
@@ -113,7 +113,20 @@
|
|
|
113
113
|
</div>
|
|
114
114
|
|
|
115
115
|
<div v-if="schemaFilters.length > 0" class="cn-index-sidebar__section">
|
|
116
|
-
|
|
116
|
+
<!-- The board look: "Alles wissen" beside the heading (DqZakenSorteren). -->
|
|
117
|
+
<div v-if="isBoardLook" class="cn-index-sidebar__filters-head" data-testid="cn-index-sidebar-filters-head">
|
|
118
|
+
<h3>{{ filtersLabel }}</h3>
|
|
119
|
+
<button
|
|
120
|
+
v-if="hasActiveFilters"
|
|
121
|
+
type="button"
|
|
122
|
+
class="cn-index-sidebar__filters-clear"
|
|
123
|
+
@click="onClearAll">
|
|
124
|
+
{{ clearAllLabel }}
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
<h3 v-else>
|
|
128
|
+
{{ filtersLabel }}
|
|
129
|
+
</h3>
|
|
117
130
|
<div
|
|
118
131
|
v-for="filter in schemaFilters"
|
|
119
132
|
:key="filter.key"
|
|
@@ -155,6 +168,27 @@
|
|
|
155
168
|
:label="filter.label"
|
|
156
169
|
:placeholder="filter.label"
|
|
157
170
|
@update:modelValue="onTextChange(filter.key, $event)" />
|
|
171
|
+
<!-- The board look: a short field as checkbox rows with counts. -->
|
|
172
|
+
<fieldset
|
|
173
|
+
v-else-if="checklistOptions(filter)"
|
|
174
|
+
class="cn-index-sidebar__checklist"
|
|
175
|
+
:data-testid="`cn-index-sidebar-checklist-${filter.key}`">
|
|
176
|
+
<legend class="hidden-visually">
|
|
177
|
+
{{ filter.label }}
|
|
178
|
+
</legend>
|
|
179
|
+
<label
|
|
180
|
+
v-for="option in checklistOptions(filter)"
|
|
181
|
+
:key="`${filter.key}-${option.id}`"
|
|
182
|
+
class="cn-index-sidebar__checklist-row"
|
|
183
|
+
data-testid="cn-index-sidebar-checklist-row">
|
|
184
|
+
<input
|
|
185
|
+
type="checkbox"
|
|
186
|
+
:checked="isChecked(filter, option.id)"
|
|
187
|
+
@change="onChecklistToggle(filter, option.id, $event.target.checked)">
|
|
188
|
+
<span class="cn-index-sidebar__checklist-label">{{ option.label }}</span>
|
|
189
|
+
<span v-if="option.count !== null" class="cn-index-sidebar__checklist-count">{{ option.count }}</span>
|
|
190
|
+
</label>
|
|
191
|
+
</fieldset>
|
|
158
192
|
<NcSelect
|
|
159
193
|
v-else
|
|
160
194
|
class="cn-index-sidebar__select"
|
|
@@ -369,7 +403,7 @@ import CnSavedViewsPanel from '../CnSavedViewsPanel/CnSavedViewsPanel.vue'
|
|
|
369
403
|
import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
|
|
370
404
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
371
405
|
import { METADATA_COLUMNS } from '../../constants/metadata.js'
|
|
372
|
-
import { facetOptions } from '../../utils/facets.js'
|
|
406
|
+
import { facetOptions, MISSING_VALUE } from '../../utils/facets.js'
|
|
373
407
|
import { labelLang } from '../../utils/manifestTranslate.js'
|
|
374
408
|
import { columnsFromSchema, filtersFromSchema } from '../../utils/schema.js'
|
|
375
409
|
/**
|
|
@@ -389,6 +423,9 @@ import { labelSidePanelClose } from '../../utils/sidePanelCloseLabel.js'
|
|
|
389
423
|
import { reverseSortKeys, sameSortKeys } from '../../utils/sortOrder.js'
|
|
390
424
|
import { CnDateRangePicker, DEFAULT_DATE_RANGE_PRESETS } from '../CnDateRangePicker/index.js'
|
|
391
425
|
import { CnIcon } from '../CnIcon/index.js'
|
|
426
|
+
/** The most values a filter shows as checkbox rows in the board look; a longer field keeps its picker. */
|
|
427
|
+
const CHECKLIST_MAX = 12
|
|
428
|
+
|
|
392
429
|
export default {
|
|
393
430
|
name: 'CnIndexSidebar',
|
|
394
431
|
|
|
@@ -1411,6 +1448,67 @@ export default {
|
|
|
1411
1448
|
return values.map((v) => options.find((o) => o.id === v) || { id: v, label: String(v) })
|
|
1412
1449
|
},
|
|
1413
1450
|
|
|
1451
|
+
/**
|
|
1452
|
+
* A field's values as checkbox rows, in the board look and for a short
|
|
1453
|
+
* list only: label and count apart, from the facet when there is one,
|
|
1454
|
+
* else the field's own options. Null where the picker stays (the
|
|
1455
|
+
* Nextcloud look, a single choice, a reference, more than
|
|
1456
|
+
* CHECKLIST_MAX values).
|
|
1457
|
+
*
|
|
1458
|
+
* @spec openspec/changes/board-sidebar-filters-as-checkbox-lists/specs/index-sidebar/spec.md#requirement-the-board-look-draws-a-short-filter-as-a-checkbox-list-with-counts
|
|
1459
|
+
* @param {object} filter The filter definition.
|
|
1460
|
+
* @return {Array<{id: (string|number|boolean), label: string, count: (number|null)}>|null} The rows, or null.
|
|
1461
|
+
*/
|
|
1462
|
+
checklistOptions(filter) {
|
|
1463
|
+
if (!this.isBoardLook || filter.multiple === false || filter.type === 'reference') {
|
|
1464
|
+
return null
|
|
1465
|
+
}
|
|
1466
|
+
const facet = this.facetData[filter.key]
|
|
1467
|
+
let rows
|
|
1468
|
+
if (facet && Array.isArray(facet.values) && facet.values.length > 0) {
|
|
1469
|
+
rows = facet.values.map((bucket) => ({
|
|
1470
|
+
id: bucket.value,
|
|
1471
|
+
label: String(bucket.label !== undefined && bucket.label !== null ? bucket.label : bucket.value),
|
|
1472
|
+
count: typeof bucket.count === 'number' ? bucket.count : null,
|
|
1473
|
+
}))
|
|
1474
|
+
if (typeof facet.missing === 'number' && facet.missing > 0) {
|
|
1475
|
+
rows.push({ id: MISSING_VALUE, label: t('nextcloud-vue', 'No value'), count: facet.missing })
|
|
1476
|
+
}
|
|
1477
|
+
} else {
|
|
1478
|
+
rows = (filter.options || []).map((option) => ({ id: option.id, label: String(option.label), count: null }))
|
|
1479
|
+
}
|
|
1480
|
+
return rows.length > 0 && rows.length <= CHECKLIST_MAX ? rows : null
|
|
1481
|
+
},
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* Whether a value of a field is among its active filter values.
|
|
1485
|
+
*
|
|
1486
|
+
* @param {object} filter The filter definition.
|
|
1487
|
+
* @param {string|number|boolean} id The value.
|
|
1488
|
+
* @return {boolean}
|
|
1489
|
+
*/
|
|
1490
|
+
isChecked(filter, id) {
|
|
1491
|
+
const value = this.activeFilters[filter.key]
|
|
1492
|
+
const values = Array.isArray(value) ? value : (value === undefined || value === null || value === '' ? [] : [value])
|
|
1493
|
+
return values.map(String).includes(String(id))
|
|
1494
|
+
},
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* A checkbox row was toggled: the field's values with it added or taken out.
|
|
1498
|
+
*
|
|
1499
|
+
* @spec openspec/changes/board-sidebar-filters-as-checkbox-lists/specs/index-sidebar/spec.md#requirement-the-board-look-draws-a-short-filter-as-a-checkbox-list-with-counts
|
|
1500
|
+
* @param {object} filter The filter definition.
|
|
1501
|
+
* @param {string|number|boolean} id The value.
|
|
1502
|
+
* @param {boolean} checked Whether it is now ticked.
|
|
1503
|
+
* @return {void}
|
|
1504
|
+
*/
|
|
1505
|
+
onChecklistToggle(filter, id, checked) {
|
|
1506
|
+
const value = this.activeFilters[filter.key]
|
|
1507
|
+
const current = Array.isArray(value) ? [...value] : (value === undefined || value === null || value === '' ? [] : [value])
|
|
1508
|
+
const rest = current.filter((v) => String(v) !== String(id))
|
|
1509
|
+
this.$emit('filter-change', { key: filter.key, values: checked ? [...rest, id] : rest })
|
|
1510
|
+
},
|
|
1511
|
+
|
|
1414
1512
|
/**
|
|
1415
1513
|
* Handle filter select change
|
|
1416
1514
|
*
|
|
@@ -226,6 +226,25 @@ function controlIcon(path) {
|
|
|
226
226
|
* @slot popup Per-marker popup body override. Scoped props: `{ feature, properties }`. When omitted, the widget renders the value of `markers.popupField` (or no popup at all when unset).
|
|
227
227
|
* @slot fallback Replaces the default "map unavailable" message when Leaflet fails to load (test environments, CSP-blocked builds, offline dev). No scoped props.
|
|
228
228
|
*/
|
|
229
|
+
/**
|
|
230
|
+
* The count line a header spec gives (`{ one, other }` or one string, with
|
|
231
|
+
* `{total}`), once the marker source answered with markers.
|
|
232
|
+
*
|
|
233
|
+
* @param {object|string|undefined} spec The declared line.
|
|
234
|
+
* @param {object} vm The map: `markersLoaded`, `markersFailed`, `markerCount`, `cnTranslate`.
|
|
235
|
+
* @return {string} The line, or ''.
|
|
236
|
+
*/
|
|
237
|
+
function countLine(spec, vm) {
|
|
238
|
+
if (!spec || !vm.markersLoaded || vm.markersFailed || vm.markerCount === 0) {
|
|
239
|
+
return ''
|
|
240
|
+
}
|
|
241
|
+
const text = typeof spec === 'string' ? spec : (vm.markerCount === 1 ? spec.one : spec.other)
|
|
242
|
+
if (typeof text !== 'string' || text === '') {
|
|
243
|
+
return ''
|
|
244
|
+
}
|
|
245
|
+
return vm.cnTranslate(text).replace(/\{total\}/g, String(vm.markerCount))
|
|
246
|
+
}
|
|
247
|
+
|
|
229
248
|
export default {
|
|
230
249
|
name: 'CnMapWidget',
|
|
231
250
|
|
|
@@ -577,15 +596,19 @@ export default {
|
|
|
577
596
|
* @spec openspec/changes/map-widget-header-count/specs/map-widget-header-count/spec.md#requirement-a-map-card-can-say-how-many-places-it-shows
|
|
578
597
|
*/
|
|
579
598
|
headerSubtitleText() {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
599
|
+
return countLine(this.content?.headerSubtitle, this)
|
|
600
|
+
},
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* The same count as a note at the right of the card's title row
|
|
604
|
+
* (`content.headerNote`, the same shape as `headerSubtitle`), as the
|
|
605
|
+
* case boards draw "Locaties ... 1 locatie".
|
|
606
|
+
*
|
|
607
|
+
* @return {string}
|
|
608
|
+
* @spec openspec/changes/map-count-note/specs/map-widget/spec.md
|
|
609
|
+
*/
|
|
610
|
+
headerNoteText() {
|
|
611
|
+
return countLine(this.content?.headerNote, this)
|
|
589
612
|
},
|
|
590
613
|
|
|
591
614
|
markerStatusText() {
|
|
@@ -672,6 +695,21 @@ export default {
|
|
|
672
695
|
}
|
|
673
696
|
},
|
|
674
697
|
|
|
698
|
+
/**
|
|
699
|
+
* Report the count note to the card around the map.
|
|
700
|
+
*
|
|
701
|
+
* @param {string} note The note, or ''.
|
|
702
|
+
*/
|
|
703
|
+
headerNoteText(note) {
|
|
704
|
+
if (this.content?.headerNote) {
|
|
705
|
+
/**
|
|
706
|
+
* @event header The card header's note at the right of the title.
|
|
707
|
+
* @type {{note: string}}
|
|
708
|
+
*/
|
|
709
|
+
this.$emit('header', { note })
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
|
|
675
713
|
// Keyed, never `deep: true` — see `layersKey`.
|
|
676
714
|
layersKey() {
|
|
677
715
|
if (this.map) {
|
|
@@ -720,6 +720,7 @@ export default {
|
|
|
720
720
|
* - `hidden` (boolean) — Hide this property
|
|
721
721
|
* - `editable` (boolean) — Override editability (default: based on schema readOnly)
|
|
722
722
|
* - `label` (string) — Override the display label
|
|
723
|
+
* - `emptyValue` (string) — Words shown, translated, for an empty or false value instead of a dash
|
|
723
724
|
* - `widget` (string) — Override the widget type for editing
|
|
724
725
|
* - `formatter` (string) — Display the value through the formatter
|
|
725
726
|
* registered under this id on CnAppRoot (`cnFormatters`), called as
|
|
@@ -739,7 +740,8 @@ export default {
|
|
|
739
740
|
},
|
|
740
741
|
|
|
741
742
|
/**
|
|
742
|
-
* Number of grid columns.
|
|
743
|
+
* Number of grid columns. The board look fits 200px columns instead,
|
|
744
|
+
* except for one column, which it keeps.
|
|
743
745
|
*/
|
|
744
746
|
columns: {
|
|
745
747
|
type: Number,
|
|
@@ -1100,7 +1102,8 @@ export default {
|
|
|
1100
1102
|
// still showing the stored code — `submitted` under a Dutch
|
|
1101
1103
|
// label, beside a list that already read "Ingediend".
|
|
1102
1104
|
const enumLabel = this.enumDisplayLabel(field, raw)
|
|
1103
|
-
|
|
1105
|
+
const empty = this.emptyValueFor(field, raw)
|
|
1106
|
+
values[field.key] = empty !== null ? empty : (enumLabel !== null ? enumLabel : formatValue(raw, prop || {}))
|
|
1104
1107
|
}
|
|
1105
1108
|
return values
|
|
1106
1109
|
},
|
|
@@ -1116,8 +1119,12 @@ export default {
|
|
|
1116
1119
|
* CSS grid template for the container.
|
|
1117
1120
|
*/
|
|
1118
1121
|
gridStyle() {
|
|
1119
|
-
// The board look: as many 200px columns as the card has room for
|
|
1122
|
+
// The board look: as many 200px columns as the card has room for,
|
|
1123
|
+
// unless the config asks for one column (a stacked list of pairs).
|
|
1120
1124
|
if (normalizeLook(this.cnLook) === 'board') {
|
|
1125
|
+
if (this.columns === 1) {
|
|
1126
|
+
return { 'grid-template-columns': 'minmax(0, 1fr)' }
|
|
1127
|
+
}
|
|
1121
1128
|
return {
|
|
1122
1129
|
'grid-template-columns': 'repeat(auto-fit, minmax(200px, 1fr))',
|
|
1123
1130
|
}
|
|
@@ -1842,6 +1849,24 @@ export default {
|
|
|
1842
1849
|
return this.isPlainObject(raw)
|
|
1843
1850
|
},
|
|
1844
1851
|
|
|
1852
|
+
/**
|
|
1853
|
+
* The words an override gives an empty or false value
|
|
1854
|
+
* (`overrides[key].emptyValue`, translated), or null to format it as usual.
|
|
1855
|
+
*
|
|
1856
|
+
* @param {object} field - Resolved field definition from resolvedFields
|
|
1857
|
+
* @param {*} raw - The field's value
|
|
1858
|
+
* @return {string|null}
|
|
1859
|
+
* @spec openspec/changes/data-widget-empty-value-one-column/specs/data-widget/spec.md
|
|
1860
|
+
*/
|
|
1861
|
+
emptyValueFor(field, raw) {
|
|
1862
|
+
const override = this.resolvedOverrides[field.key]
|
|
1863
|
+
const words = override && typeof override.emptyValue === 'string' ? override.emptyValue : ''
|
|
1864
|
+
if (words === '' || !(raw === null || raw === undefined || raw === '' || raw === false)) {
|
|
1865
|
+
return null
|
|
1866
|
+
}
|
|
1867
|
+
return this.cnTranslate(words)
|
|
1868
|
+
},
|
|
1869
|
+
|
|
1845
1870
|
/**
|
|
1846
1871
|
* Check if a field is editable.
|
|
1847
1872
|
*
|