@conduction/nextcloud-vue 2.96.0 → 2.98.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 +17 -0
- 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 +33 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +383 -359
- 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 +106 -2
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +9 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +964 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +282 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChecklistRunWidget/index.js +28 -0
- package/dist/esm/components/CnChecklistRunWidget/index.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +17 -3
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +61 -43
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +159 -18
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +9 -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 +26 -8
- 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 +188 -5
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +48 -3
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +46 -12
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +3 -3
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +27 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +16 -2
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +144 -0
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +488 -399
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +344 -21
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +29 -7
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +13 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +5 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +9 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js.map +1 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +220 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +121 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +83 -9
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +80 -11
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +420 -5
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useOrganisationStore.js +115 -0
- package/dist/esm/composables/useOrganisationStore.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +21 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +36 -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 +725 -383
- package/dist/esm/utils/builtInFormatters.js +34 -1
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/matrixCells.js +158 -0
- package/dist/esm/utils/matrixCells.js.map +1 -0
- package/dist/esm/utils/nextcloudDirectory.js +121 -0
- package/dist/esm/utils/nextcloudDirectory.js.map +1 -0
- package/dist/esm/utils/queryPresets.js +13 -2
- package/dist/esm/utils/queryPresets.js.map +1 -1
- package/dist/esm/utils/scopeListLayout.js +37 -1
- package/dist/esm/utils/scopeListLayout.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 +14443 -10519
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +725 -383
- package/l10n/en.json +21 -1
- package/l10n/nl.json +36 -1
- package/package.json +2 -2
- package/scripts/merge-l10n.js +196 -0
- package/src/components/CnAppRoot/CnAppRoot.md +1 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +17 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +57 -1
- package/src/components/CnChartWidget/CnChartWidget.md +2 -1
- package/src/components/CnChartWidget/CnChartWidget.vue +106 -2
- package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +1269 -0
- package/src/components/CnChecklistRunWidget/index.js +27 -0
- package/src/components/CnConversationThread/CnConversationThread.md +12 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +30 -4
- package/src/components/CnConversationThread/CnConversationWidget.vue +167 -24
- package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +26 -8
- package/src/components/CnDetailPage/CnDetailPage.md +48 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +188 -5
- package/src/components/CnFactsWidget/CnFactsWidget.vue +114 -3
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +3 -3
- package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +38 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +508 -236
- package/src/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
- package/src/components/CnIndexPage/CnIndexPage.md +28 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +367 -27
- package/src/components/CnIndexPage/CnIndexPageWidgets.vue +13 -4
- package/src/components/CnIndexPage/useNamedSource.js +1 -1
- package/src/components/CnObjectSidebar/CnFilesTab.vue +17 -0
- package/src/components/CnOrderedListField/CnOrderedListField.vue +386 -0
- package/src/components/CnOrderedListField/index.js +7 -0
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +139 -10
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/index.js +2 -0
- package/src/composables/indexSources.js +419 -4
- package/src/composables/useOrganisationStore.js +111 -0
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +42 -5
- package/src/utils/builtInFormatters.js +33 -0
- package/src/utils/libraryWidgetKeys.js +1 -0
- package/src/utils/matrixCells.js +154 -0
- package/src/utils/nextcloudDirectory.js +117 -0
- package/src/utils/queryPresets.js +13 -2
- package/src/utils/scopeListLayout.js +37 -1
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
-->
|
|
12
12
|
<template>
|
|
13
13
|
<component
|
|
14
|
-
:is="variant === 'stat-row' ? 'CnKpiGrid' : 'aside'"
|
|
14
|
+
:is="variant === 'stat-row' ? 'CnKpiGrid' : (variant === 'after-list' ? 'section' : 'aside')"
|
|
15
15
|
v-if="entries.length > 0"
|
|
16
16
|
:columns="variant === 'stat-row' ? 'auto' : undefined"
|
|
17
17
|
class="cn-index-page-widgets"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<CnWidgetWrapper
|
|
26
26
|
:widgetId="entry.id"
|
|
27
27
|
:title="entry.title"
|
|
28
|
-
:showTitle="variant
|
|
28
|
+
:showTitle="variant !== 'stat-row' && entry.title !== ''"
|
|
29
29
|
:showActions="false"
|
|
30
30
|
:showRefresh="false"
|
|
31
31
|
:headerLink="entry.headerLink"
|
|
@@ -77,11 +77,11 @@ export default {
|
|
|
77
77
|
default: () => [],
|
|
78
78
|
},
|
|
79
79
|
|
|
80
|
-
/** Where the widgets sit: `stat-row` above the list, `side-panel` beside it. */
|
|
80
|
+
/** Where the widgets sit: `stat-row` above the list, `side-panel` beside it, `after-list` under it. */
|
|
81
81
|
variant: {
|
|
82
82
|
type: String,
|
|
83
83
|
default: 'stat-row',
|
|
84
|
-
validator: (v) => ['stat-row', 'side-panel'].includes(v),
|
|
84
|
+
validator: (v) => ['stat-row', 'side-panel', 'after-list'].includes(v),
|
|
85
85
|
},
|
|
86
86
|
},
|
|
87
87
|
|
|
@@ -172,6 +172,15 @@ export default {
|
|
|
172
172
|
min-width: 0;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/* The cards after the list, stacked full width (PqSocial). */
|
|
176
|
+
.cn-index-page-widgets--after-list {
|
|
177
|
+
display: flex;
|
|
178
|
+
flex-direction: column;
|
|
179
|
+
gap: var(--cn-board-section-gap, 16px);
|
|
180
|
+
margin-top: var(--cn-board-section-gap, 16px);
|
|
181
|
+
min-width: 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
175
184
|
/* The side panel column (DqTeamwachtrij: 300px, 16px between cards). Its
|
|
176
185
|
place beside the toolbar and the list is set by CnIndexPage
|
|
177
186
|
(`cn-index-page--with-side-panel`, index-page.css). */
|
|
@@ -75,7 +75,7 @@ export function useNamedSource(props, options = {}) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
const source = resolveIndexSource(props.entitySource)
|
|
78
|
+
const source = resolveIndexSource(props.entitySource, props.sourceConfig)
|
|
79
79
|
|
|
80
80
|
// An unknown source name resolves to null and was already warned about.
|
|
81
81
|
// The page then behaves as an empty index rather than throwing.
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
:preferenceApp="preferenceApp"
|
|
22
22
|
:preferenceKey="preferenceKey"
|
|
23
23
|
:bulkActions="bulkActions"
|
|
24
|
+
:selectionLabel="selectionLabel"
|
|
25
|
+
:selectionLabelOne="selectionLabelOne"
|
|
26
|
+
:showClearSelection="showClearSelection"
|
|
27
|
+
:sideCards="sideCards"
|
|
24
28
|
:groupBy="groupBy"
|
|
25
29
|
:facets="facets"
|
|
26
30
|
@changed="onBrowserChanged" />
|
|
@@ -394,6 +398,19 @@ export default {
|
|
|
394
398
|
* @type {Array<{id: string, label: string, icon?: string, type?: string, target?: string, props?: object}>}
|
|
395
399
|
*/
|
|
396
400
|
bulkActions: { type: Array, default: () => [] },
|
|
401
|
+
/** Forwarded to CnFilesBrowser: the bulk bar's count, a template with `{count}`. */
|
|
402
|
+
selectionLabel: { type: String, default: '' },
|
|
403
|
+
/** Forwarded to CnFilesBrowser: the bulk bar's count for one file. */
|
|
404
|
+
selectionLabelOne: { type: String, default: '' },
|
|
405
|
+
/** Forwarded to CnFilesBrowser: whether the bulk bar ends in "Clear selection". */
|
|
406
|
+
showClearSelection: { type: Boolean, default: true },
|
|
407
|
+
/**
|
|
408
|
+
* Forwarded to CnFilesBrowser: cards beside the list about the last
|
|
409
|
+
* selected file.
|
|
410
|
+
*
|
|
411
|
+
* @type {Array<{id: string, title: string, fields?: Array<object>, actions?: Array<object>}>}
|
|
412
|
+
*/
|
|
413
|
+
sideCards: { type: Array, default: () => [] },
|
|
397
414
|
/** Forwarded to CnFilesBrowser: the key of a declared column to group the files on. */
|
|
398
415
|
groupBy: { type: String, default: '' },
|
|
399
416
|
/**
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div class="cn-ordered-list-field" data-testid="cn-ordered-list-field">
|
|
8
|
+
<p v-if="label" :id="labelId" class="cn-ordered-list-field__label">
|
|
9
|
+
{{ label }}
|
|
10
|
+
</p>
|
|
11
|
+
<ol
|
|
12
|
+
v-if="rows.length > 0"
|
|
13
|
+
class="cn-ordered-list-field__rows"
|
|
14
|
+
:aria-labelledby="label ? labelId : null"
|
|
15
|
+
:aria-label="label ? null : listLabel">
|
|
16
|
+
<li
|
|
17
|
+
v-for="(row, index) in rows"
|
|
18
|
+
:key="row.value"
|
|
19
|
+
class="cn-ordered-list-field__row"
|
|
20
|
+
:data-testid="`cn-ordered-list-row-${row.value}`">
|
|
21
|
+
<span class="cn-ordered-list-field__position" aria-hidden="true">{{ index + 1 }}</span>
|
|
22
|
+
<span class="cn-ordered-list-field__text">{{ row.label }}</span>
|
|
23
|
+
<span class="cn-ordered-list-field__actions">
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
class="cn-ordered-list-field__button"
|
|
27
|
+
:disabled="disabled || index === 0"
|
|
28
|
+
:aria-label="t('nextcloud-vue', 'Move {item} up', { item: row.label })"
|
|
29
|
+
:data-testid="`cn-ordered-list-up-${row.value}`"
|
|
30
|
+
@click="move(index, index - 1)">
|
|
31
|
+
<ChevronUp :size="20" />
|
|
32
|
+
</button>
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
class="cn-ordered-list-field__button"
|
|
36
|
+
:disabled="disabled || index === rows.length - 1"
|
|
37
|
+
:aria-label="t('nextcloud-vue', 'Move {item} down', { item: row.label })"
|
|
38
|
+
:data-testid="`cn-ordered-list-down-${row.value}`"
|
|
39
|
+
@click="move(index, index + 1)">
|
|
40
|
+
<ChevronDown :size="20" />
|
|
41
|
+
</button>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
class="cn-ordered-list-field__button"
|
|
45
|
+
:disabled="disabled"
|
|
46
|
+
:aria-label="t('nextcloud-vue', 'Remove {item}', { item: row.label })"
|
|
47
|
+
:data-testid="`cn-ordered-list-remove-${row.value}`"
|
|
48
|
+
@click="remove(index)">
|
|
49
|
+
<Close :size="20" />
|
|
50
|
+
</button>
|
|
51
|
+
</span>
|
|
52
|
+
</li>
|
|
53
|
+
</ol>
|
|
54
|
+
<p v-else class="cn-ordered-list-field__empty" data-testid="cn-ordered-list-empty">
|
|
55
|
+
{{ emptyText || t('nextcloud-vue', 'Nothing added yet.') }}
|
|
56
|
+
</p>
|
|
57
|
+
<div v-if="!disabled" class="cn-ordered-list-field__add">
|
|
58
|
+
<NcSelect
|
|
59
|
+
:modelValue="null"
|
|
60
|
+
:options="available"
|
|
61
|
+
label="label"
|
|
62
|
+
:inputLabel="addLabel || t('nextcloud-vue', 'Add')"
|
|
63
|
+
:placeholder="addPlaceholder"
|
|
64
|
+
:loading="loading"
|
|
65
|
+
:clearable="false"
|
|
66
|
+
:filterable="!searchable"
|
|
67
|
+
data-testid="cn-ordered-list-add"
|
|
68
|
+
@search="onSearch"
|
|
69
|
+
@update:modelValue="add" />
|
|
70
|
+
</div>
|
|
71
|
+
<p class="hidden-visually" aria-live="polite" data-testid="cn-ordered-list-live">
|
|
72
|
+
{{ announcement }}
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<script>
|
|
78
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
79
|
+
import { NcSelect } from '@nextcloud/vue'
|
|
80
|
+
import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
|
|
81
|
+
import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
|
|
82
|
+
import Close from 'vue-material-design-icons/Close.vue'
|
|
83
|
+
|
|
84
|
+
let orderedListUid = 0
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* CnOrderedListField - an ordered list a user keeps for themselves: rows in
|
|
88
|
+
* their order with a move up, a move down and a remove button each, and a
|
|
89
|
+
* search-as-you-type picker under it that adds a row (board
|
|
90
|
+
* DqPersoonlijkeInstellingen, "Zaaktypen in mijn menu").
|
|
91
|
+
*
|
|
92
|
+
* The app supplies the choices (`options`) and keeps the value: every change
|
|
93
|
+
* emits `update:modelValue` with the new list of values. With `searchable`
|
|
94
|
+
* the picker emits `search` as the user types, so the app can fetch matching
|
|
95
|
+
* choices itself; otherwise it filters `options`. Every move is announced in a
|
|
96
|
+
* polite live region, so reordering works with the keyboard and a screen
|
|
97
|
+
* reader, not by dragging.
|
|
98
|
+
*
|
|
99
|
+
* ```vue
|
|
100
|
+
* <CnOrderedListField
|
|
101
|
+
* v-model="menuCaseTypes"
|
|
102
|
+
* label="Case types in my menu"
|
|
103
|
+
* add-label="Add case type"
|
|
104
|
+
* :options="caseTypes" />
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* @spec openspec/changes/ordered-list-field/specs/ordered-list-field/spec.md#requirement-a-user-keeps-an-ordered-list-with-the-keyboard
|
|
108
|
+
*/
|
|
109
|
+
export default {
|
|
110
|
+
name: 'CnOrderedListField',
|
|
111
|
+
|
|
112
|
+
components: { ChevronDown, ChevronUp, Close, NcSelect },
|
|
113
|
+
|
|
114
|
+
props: {
|
|
115
|
+
/** The values in their order. */
|
|
116
|
+
modelValue: {
|
|
117
|
+
type: Array,
|
|
118
|
+
default: () => [],
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The choices, `{ value, label }` (a plain string is its own label).
|
|
123
|
+
* A value in `modelValue` without a choice shows the value itself.
|
|
124
|
+
*/
|
|
125
|
+
options: {
|
|
126
|
+
type: Array,
|
|
127
|
+
default: () => [],
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/** The list's label, also its accessible name. */
|
|
131
|
+
label: {
|
|
132
|
+
type: String,
|
|
133
|
+
default: '',
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/** The picker's label. */
|
|
137
|
+
addLabel: {
|
|
138
|
+
type: String,
|
|
139
|
+
default: '',
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/** Placeholder in the picker. */
|
|
143
|
+
addPlaceholder: {
|
|
144
|
+
type: String,
|
|
145
|
+
default: '',
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/** Shown when the list is empty. */
|
|
149
|
+
emptyText: {
|
|
150
|
+
type: String,
|
|
151
|
+
default: '',
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/** Whether the app searches (`search` event) instead of the picker filtering `options`. */
|
|
155
|
+
searchable: {
|
|
156
|
+
type: Boolean,
|
|
157
|
+
default: false,
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/** Whether the app is fetching choices. */
|
|
161
|
+
loading: {
|
|
162
|
+
type: Boolean,
|
|
163
|
+
default: false,
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/** A read-only list: no buttons and no picker. */
|
|
167
|
+
disabled: {
|
|
168
|
+
type: Boolean,
|
|
169
|
+
default: false,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
emits: ['update:modelValue', 'search'],
|
|
174
|
+
|
|
175
|
+
data() {
|
|
176
|
+
orderedListUid += 1
|
|
177
|
+
return {
|
|
178
|
+
labelId: `cn-ordered-list-label-${orderedListUid}`,
|
|
179
|
+
announcement: '',
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
computed: {
|
|
184
|
+
/** @return {Array<{value: string, label: string}>} The choices in one shape. */
|
|
185
|
+
choices() {
|
|
186
|
+
return (Array.isArray(this.options) ? this.options : [])
|
|
187
|
+
.map((option) => (option && typeof option === 'object'
|
|
188
|
+
? { value: String(option.value ?? option.id ?? ''), label: String(option.label ?? option.title ?? option.value ?? '') }
|
|
189
|
+
: { value: String(option), label: String(option) }))
|
|
190
|
+
.filter((option) => option.value !== '')
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
/** @return {Array<{value: string, label: string}>} The rows, in order. */
|
|
194
|
+
rows() {
|
|
195
|
+
return (Array.isArray(this.modelValue) ? this.modelValue : []).map((value) => {
|
|
196
|
+
const key = String(value)
|
|
197
|
+
const choice = this.choices.find((option) => option.value === key)
|
|
198
|
+
return { value: key, label: choice ? choice.label : key }
|
|
199
|
+
})
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/** @return {Array<{value: string, label: string}>} The choices not in the list yet. */
|
|
203
|
+
available() {
|
|
204
|
+
const taken = new Set(this.rows.map((row) => row.value))
|
|
205
|
+
return this.choices.filter((option) => !taken.has(option.value))
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
/** @return {string} The list's accessible name without a label. */
|
|
209
|
+
listLabel() {
|
|
210
|
+
return t('nextcloud-vue', 'Ordered list')
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
methods: {
|
|
215
|
+
t,
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Emit the new order.
|
|
219
|
+
*
|
|
220
|
+
* @param {Array<string>} values The values.
|
|
221
|
+
*/
|
|
222
|
+
emitValue(values) {
|
|
223
|
+
/**
|
|
224
|
+
* @event update:modelValue The list changed. Payload: the values in their order.
|
|
225
|
+
*/
|
|
226
|
+
this.$emit('update:modelValue', values)
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Move a row to another place and keep focus on the pressed button
|
|
231
|
+
* where it still makes sense.
|
|
232
|
+
*
|
|
233
|
+
* @param {number} from Its index.
|
|
234
|
+
* @param {number} to The index it moves to.
|
|
235
|
+
*/
|
|
236
|
+
move(from, to) {
|
|
237
|
+
if (to < 0 || to >= this.rows.length) {
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
const values = this.rows.map((row) => row.value)
|
|
241
|
+
const [value] = values.splice(from, 1)
|
|
242
|
+
values.splice(to, 0, value)
|
|
243
|
+
const row = this.rows[from]
|
|
244
|
+
this.emitValue(values)
|
|
245
|
+
this.announcement = t('nextcloud-vue', '{item} moved to position {position} of {total}', { item: row.label, position: to + 1, total: values.length })
|
|
246
|
+
this.$nextTick(() => {
|
|
247
|
+
const direction = to < from ? 'up' : 'down'
|
|
248
|
+
const atEdge = (direction === 'up' && to === 0) || (direction === 'down' && to === values.length - 1)
|
|
249
|
+
const target = this.$el.querySelector(`[data-testid="cn-ordered-list-${atEdge ? (direction === 'up' ? 'down' : 'up') : direction}-${row.value}"]`)
|
|
250
|
+
if (target && !target.disabled) {
|
|
251
|
+
target.focus()
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Remove a row.
|
|
258
|
+
*
|
|
259
|
+
* @param {number} index Its index.
|
|
260
|
+
*/
|
|
261
|
+
remove(index) {
|
|
262
|
+
const row = this.rows[index]
|
|
263
|
+
this.emitValue(this.rows.filter((_, i) => i !== index).map((r) => r.value))
|
|
264
|
+
this.announcement = t('nextcloud-vue', '{item} removed', { item: row.label })
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Add the picked choice at the end.
|
|
269
|
+
*
|
|
270
|
+
* @param {{value: string, label: string}|null} option The choice.
|
|
271
|
+
*/
|
|
272
|
+
add(option) {
|
|
273
|
+
if (!option) {
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
this.emitValue([...this.rows.map((row) => row.value), option.value])
|
|
277
|
+
this.announcement = t('nextcloud-vue', '{item} added', { item: option.label })
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Pass the typed text on when the app searches.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} query The text.
|
|
284
|
+
*/
|
|
285
|
+
onSearch(query) {
|
|
286
|
+
if (this.searchable) {
|
|
287
|
+
/**
|
|
288
|
+
* @event search The user typed in the picker (with `searchable`). Payload: the text.
|
|
289
|
+
*/
|
|
290
|
+
this.$emit('search', query)
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
</script>
|
|
296
|
+
|
|
297
|
+
<style scoped>
|
|
298
|
+
.cn-ordered-list-field {
|
|
299
|
+
display: flex;
|
|
300
|
+
flex-direction: column;
|
|
301
|
+
gap: 10px;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.cn-ordered-list-field__label {
|
|
305
|
+
margin: 0;
|
|
306
|
+
font-weight: 600;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.cn-ordered-list-field__rows {
|
|
310
|
+
display: flex;
|
|
311
|
+
flex-direction: column;
|
|
312
|
+
margin: 0;
|
|
313
|
+
padding: 0;
|
|
314
|
+
border: 1px solid var(--color-border);
|
|
315
|
+
border-radius: var(--border-radius-element, 8px);
|
|
316
|
+
list-style: none;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.cn-ordered-list-field__row {
|
|
320
|
+
display: flex;
|
|
321
|
+
align-items: center;
|
|
322
|
+
gap: 10px;
|
|
323
|
+
min-height: 44px;
|
|
324
|
+
padding: 2px 6px 2px 12px;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.cn-ordered-list-field__row + .cn-ordered-list-field__row {
|
|
328
|
+
border-top: 1px solid var(--color-border);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.cn-ordered-list-field__position {
|
|
332
|
+
flex: none;
|
|
333
|
+
min-width: 18px;
|
|
334
|
+
color: var(--color-text-maxcontrast);
|
|
335
|
+
font-size: 13px;
|
|
336
|
+
font-variant-numeric: tabular-nums;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.cn-ordered-list-field__text {
|
|
340
|
+
flex: 1 1 auto;
|
|
341
|
+
min-width: 0;
|
|
342
|
+
overflow-wrap: anywhere;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.cn-ordered-list-field__actions {
|
|
346
|
+
display: flex;
|
|
347
|
+
flex: none;
|
|
348
|
+
gap: 2px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.cn-ordered-list-field__button {
|
|
352
|
+
display: inline-flex;
|
|
353
|
+
align-items: center;
|
|
354
|
+
justify-content: center;
|
|
355
|
+
width: 40px;
|
|
356
|
+
min-width: 40px;
|
|
357
|
+
height: 40px;
|
|
358
|
+
min-height: 40px;
|
|
359
|
+
margin: 0;
|
|
360
|
+
padding: 0;
|
|
361
|
+
border: 0;
|
|
362
|
+
border-radius: var(--border-radius-element, 8px);
|
|
363
|
+
background: transparent;
|
|
364
|
+
color: var(--color-main-text);
|
|
365
|
+
cursor: pointer;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.cn-ordered-list-field__button:hover:not(:disabled) {
|
|
369
|
+
background: var(--color-background-hover);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.cn-ordered-list-field__button:focus-visible {
|
|
373
|
+
outline: 2px solid var(--color-primary-element);
|
|
374
|
+
outline-offset: 1px;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.cn-ordered-list-field__button:disabled {
|
|
378
|
+
opacity: 0.4;
|
|
379
|
+
cursor: default;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.cn-ordered-list-field__empty {
|
|
383
|
+
margin: 0;
|
|
384
|
+
color: var(--color-text-maxcontrast);
|
|
385
|
+
}
|
|
386
|
+
</style>
|