@conduction/nextcloud-vue 2.97.0 → 2.99.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/CnActionsMenu/CnActionsMenu.vue2.js +12 -0
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +11 -2
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
- 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/CnBreadcrumbs/CnBreadcrumbs.vue.js +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.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/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 +2 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.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/CnIndexPage/CnIndexPage.vue2.js +43 -3
- 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/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 +239 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +182 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +9 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js.map +1 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +265 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +191 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js +9 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js.map +1 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js +508 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js.map +1 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js +99 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js.map +1 -0
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +18 -70
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +6 -44
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
- 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 +2 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js +9 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js.map +1 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js +472 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js +231 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnWorkingCalendarWidget/index.js +27 -0
- package/dist/esm/components/CnWorkingCalendarWidget/index.js.map +1 -0
- package/dist/esm/composables/indexSources.js +238 -2
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useFlowStore.js +2 -1
- package/dist/esm/composables/useFlowStore.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 +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +109 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +138 -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 +1071 -274
- package/dist/esm/utils/builtInFormatters.js +34 -1
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +2 -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/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/workingCalendarMonth.js +85 -0
- package/dist/esm/utils/workingCalendarMonth.js.map +1 -0
- package/dist/nextcloud-vue.cjs.js +14888 -10014
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1071 -274
- package/l10n/en.json +109 -1
- package/l10n/nl.json +138 -1
- package/package.json +2 -2
- package/scripts/merge-l10n.js +196 -0
- package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
- package/src/components/CnAppRoot/CnAppRoot.md +1 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +17 -0
- package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +1 -0
- 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/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 +189 -5
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +3 -3
- package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +38 -2
- package/src/components/CnIndexPage/CnIndexPage.md +28 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +43 -3
- package/src/components/CnOrderedListField/CnOrderedListField.vue +458 -0
- package/src/components/CnOrderedListField/index.js +7 -0
- package/src/components/CnSettingsField/CnSettingsField.vue +449 -0
- package/src/components/CnSettingsField/index.js +4 -0
- package/src/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue +613 -0
- package/src/components/CnSettingsFieldsSection/index.js +4 -0
- package/src/components/CnSettingsPage/CnSettingsPage.vue +6 -86
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +139 -10
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +2 -0
- package/src/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue +725 -0
- package/src/components/CnWorkingCalendarWidget/index.js +26 -0
- package/src/components/index.js +5 -0
- package/src/composables/indexSources.js +237 -1
- package/src/composables/useFlowStore.js +2 -1
- package/src/composables/useOrganisationStore.js +111 -0
- package/src/css/badge.css +32 -16
- package/src/css/look-board-detail.css +80 -8
- package/src/css/look-board-index.css +52 -8
- package/src/css/look-board.css +219 -1
- package/src/index.js +5 -0
- package/src/schemas/app-manifest-v2.schema.json +33 -3
- package/src/utils/builtInFormatters.js +33 -0
- package/src/utils/libraryWidgetKeys.js +2 -0
- package/src/utils/matrixCells.js +154 -0
- package/src/utils/nextcloudDirectory.js +117 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/workingCalendarMonth.js +82 -0
|
@@ -1224,6 +1224,7 @@ import { isModifiedClick, openRowTarget } from '../../utils/linkNavigation.js'
|
|
|
1224
1224
|
import { resolveClaimedTeams, resolveClaimTokens, splitViewsIntoTabs, viewAsTab, viewIdOf } from '../../utils/listLenses.js'
|
|
1225
1225
|
import { LIST_SHORTCUTS, listPaletteCommands, shortcutFor } from '../../utils/listShortcuts.js'
|
|
1226
1226
|
import { multiKeySort } from '../../utils/multiKeySort.js'
|
|
1227
|
+
import { fetchGroups } from '../../utils/nextcloudDirectory.js'
|
|
1227
1228
|
import { OBJECTS_CHANGED_EVENT } from '../../utils/objectSignals.js'
|
|
1228
1229
|
import { objectUrlOf, patchObject } from '../../utils/objectWrite.js'
|
|
1229
1230
|
import { withPersonalLenses } from '../../utils/personalLenses.js'
|
|
@@ -3749,6 +3750,8 @@ export default {
|
|
|
3749
3750
|
*/
|
|
3750
3751
|
folderSidebarAllValues: [],
|
|
3751
3752
|
folderRegisterList: [],
|
|
3753
|
+
/** The Nextcloud groups a `folderSidebar.source: 'groups'` pane lists. */
|
|
3754
|
+
folderGroupList: [],
|
|
3752
3755
|
// `x-index` blocks read off the schema rows a `source: 'register'`
|
|
3753
3756
|
// folder list was built from, keyed by folder id. The layout a case
|
|
3754
3757
|
// type carries travels with the record rather than being restated
|
|
@@ -4666,7 +4669,7 @@ export default {
|
|
|
4666
4669
|
*/
|
|
4667
4670
|
folderSidebarSource() {
|
|
4668
4671
|
const s = this.folderSidebar && this.folderSidebar.source
|
|
4669
|
-
return (s === 'register' || !s) ? 'custom' : s
|
|
4672
|
+
return (s === 'register' || s === 'groups' || !s) ? 'custom' : s
|
|
4670
4673
|
},
|
|
4671
4674
|
|
|
4672
4675
|
/**
|
|
@@ -4683,6 +4686,9 @@ export default {
|
|
|
4683
4686
|
if (this.folderSidebar.source === 'register') {
|
|
4684
4687
|
return this.folderRegisterList
|
|
4685
4688
|
}
|
|
4689
|
+
if (this.folderSidebar.source === 'groups') {
|
|
4690
|
+
return this.folderGroupList
|
|
4691
|
+
}
|
|
4686
4692
|
return this.folderSidebar.folders || []
|
|
4687
4693
|
},
|
|
4688
4694
|
|
|
@@ -5448,7 +5454,7 @@ export default {
|
|
|
5448
5454
|
* @return {string} The id field key.
|
|
5449
5455
|
*/
|
|
5450
5456
|
folderPassthroughIdField() {
|
|
5451
|
-
if (this.folderSidebar && this.folderSidebar.source === 'register') {
|
|
5457
|
+
if (this.folderSidebar && (this.folderSidebar.source === 'register' || this.folderSidebar.source === 'groups')) {
|
|
5452
5458
|
return 'id'
|
|
5453
5459
|
}
|
|
5454
5460
|
return (this.folderSidebar && this.folderSidebar.idField) || 'id'
|
|
@@ -5458,7 +5464,7 @@ export default {
|
|
|
5458
5464
|
* @return {string} The name field key for CnFolderSidebar's custom list.
|
|
5459
5465
|
*/
|
|
5460
5466
|
folderPassthroughNameField() {
|
|
5461
|
-
if (this.folderSidebar && this.folderSidebar.source === 'register') {
|
|
5467
|
+
if (this.folderSidebar && (this.folderSidebar.source === 'register' || this.folderSidebar.source === 'groups')) {
|
|
5462
5468
|
return 'name'
|
|
5463
5469
|
}
|
|
5464
5470
|
return (this.folderSidebar && this.folderSidebar.nameField) || 'name'
|
|
@@ -7113,6 +7119,7 @@ export default {
|
|
|
7113
7119
|
this.maybeOpenCreateFromQuery()
|
|
7114
7120
|
if (this.folderSidebar) {
|
|
7115
7121
|
this.loadFolderRegister()
|
|
7122
|
+
this.loadFolderGroups()
|
|
7116
7123
|
// Reflect a deep-link filter (e.g. ?caseType=<id>) as the active folder.
|
|
7117
7124
|
const key = this.folderSidebar.filterField || this.folderSidebar.field
|
|
7118
7125
|
const active = key && this.effectiveActiveFilters[key]
|
|
@@ -8985,6 +8992,39 @@ export default {
|
|
|
8985
8992
|
}
|
|
8986
8993
|
},
|
|
8987
8994
|
|
|
8995
|
+
/**
|
|
8996
|
+
* Load the Nextcloud groups for `folderSidebar.source: 'groups'`, each
|
|
8997
|
+
* with its member count. `folderSidebar.groups` (`[{ id, parent? }]`)
|
|
8998
|
+
* picks and orders them and nests a group under its `parent`; without
|
|
8999
|
+
* it every group shows, by name. A refused request lists none.
|
|
9000
|
+
*
|
|
9001
|
+
* @return {Promise<void>}
|
|
9002
|
+
* @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members
|
|
9003
|
+
*/
|
|
9004
|
+
async loadFolderGroups() {
|
|
9005
|
+
const cfg = this.folderSidebar
|
|
9006
|
+
if (!cfg || cfg.source !== 'groups') {
|
|
9007
|
+
return
|
|
9008
|
+
}
|
|
9009
|
+
try {
|
|
9010
|
+
const { default: axios } = await import('@nextcloud/axios')
|
|
9011
|
+
const groups = await fetchGroups(axios, { search: cfg.search || '', limit: cfg.limit || 200 })
|
|
9012
|
+
const declared = Array.isArray(cfg.groups) ? cfg.groups.filter((entry) => entry && entry.id) : null
|
|
9013
|
+
if (!declared) {
|
|
9014
|
+
this.folderGroupList = [...groups].sort((a, b) => a.name.localeCompare(b.name))
|
|
9015
|
+
return
|
|
9016
|
+
}
|
|
9017
|
+
const byId = new Map(groups.map((group) => [group.id, group]))
|
|
9018
|
+
this.folderGroupList = declared
|
|
9019
|
+
.filter((entry) => byId.has(String(entry.id)))
|
|
9020
|
+
.map((entry) => ({ ...byId.get(String(entry.id)), parentId: entry.parent ? String(entry.parent) : null }))
|
|
9021
|
+
} catch (e) {
|
|
9022
|
+
// eslint-disable-next-line no-console
|
|
9023
|
+
console.error('[CnIndexPage] failed to load the Nextcloud groups', e)
|
|
9024
|
+
this.folderGroupList = []
|
|
9025
|
+
}
|
|
9026
|
+
},
|
|
9027
|
+
|
|
8988
9028
|
/**
|
|
8989
9029
|
* Load the folder list from an OpenRegister register/schema for a
|
|
8990
9030
|
* `folderSidebar.source === 'register'` config. Maps each object to
|
|
@@ -0,0 +1,458 @@
|
|
|
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 v-if="row.icon" class="cn-ordered-list-field__tile" aria-hidden="true">
|
|
23
|
+
<CnIcon :name="row.icon" :size="18" />
|
|
24
|
+
</span>
|
|
25
|
+
<span class="cn-ordered-list-field__text">
|
|
26
|
+
<span class="cn-ordered-list-field__name">{{ row.label }}</span>
|
|
27
|
+
<span v-if="row.description"
|
|
28
|
+
:id="descriptionId(row)"
|
|
29
|
+
class="cn-ordered-list-field__description"
|
|
30
|
+
:data-testid="`cn-ordered-list-description-${row.value}`">{{ row.description }}</span>
|
|
31
|
+
</span>
|
|
32
|
+
<span class="cn-ordered-list-field__actions">
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
class="cn-ordered-list-field__button"
|
|
36
|
+
:disabled="disabled || index === 0"
|
|
37
|
+
:aria-label="t('nextcloud-vue', 'Move {item} up', { item: row.label })"
|
|
38
|
+
:data-testid="`cn-ordered-list-up-${row.value}`"
|
|
39
|
+
:aria-describedby="row.description ? descriptionId(row) : null"
|
|
40
|
+
@click="move(index, index - 1)">
|
|
41
|
+
<ChevronUp :size="20" />
|
|
42
|
+
</button>
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
class="cn-ordered-list-field__button"
|
|
46
|
+
:disabled="disabled || index === rows.length - 1"
|
|
47
|
+
:aria-label="t('nextcloud-vue', 'Move {item} down', { item: row.label })"
|
|
48
|
+
:data-testid="`cn-ordered-list-down-${row.value}`"
|
|
49
|
+
:aria-describedby="row.description ? descriptionId(row) : null"
|
|
50
|
+
@click="move(index, index + 1)">
|
|
51
|
+
<ChevronDown :size="20" />
|
|
52
|
+
</button>
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
class="cn-ordered-list-field__button"
|
|
56
|
+
:disabled="disabled"
|
|
57
|
+
:aria-label="t('nextcloud-vue', 'Remove {item}', { item: row.label })"
|
|
58
|
+
:data-testid="`cn-ordered-list-remove-${row.value}`"
|
|
59
|
+
:aria-describedby="row.description ? descriptionId(row) : null"
|
|
60
|
+
@click="remove(index)">
|
|
61
|
+
<Close :size="20" />
|
|
62
|
+
</button>
|
|
63
|
+
</span>
|
|
64
|
+
</li>
|
|
65
|
+
</ol>
|
|
66
|
+
<p v-else class="cn-ordered-list-field__empty" data-testid="cn-ordered-list-empty">
|
|
67
|
+
{{ emptyText || t('nextcloud-vue', 'Nothing added yet.') }}
|
|
68
|
+
</p>
|
|
69
|
+
<div v-if="!disabled" class="cn-ordered-list-field__add">
|
|
70
|
+
<NcSelect
|
|
71
|
+
:modelValue="null"
|
|
72
|
+
:options="available"
|
|
73
|
+
label="label"
|
|
74
|
+
:inputLabel="addLabel || t('nextcloud-vue', 'Add')"
|
|
75
|
+
:placeholder="addPlaceholder"
|
|
76
|
+
:loading="loading"
|
|
77
|
+
:clearable="false"
|
|
78
|
+
:filterable="!searchable"
|
|
79
|
+
data-testid="cn-ordered-list-add"
|
|
80
|
+
@search="onSearch"
|
|
81
|
+
@update:modelValue="add">
|
|
82
|
+
<template #option="option">
|
|
83
|
+
<span class="cn-ordered-list-field__option">
|
|
84
|
+
<span v-if="option.icon" class="cn-ordered-list-field__tile" aria-hidden="true">
|
|
85
|
+
<CnIcon :name="option.icon" :size="18" />
|
|
86
|
+
</span>
|
|
87
|
+
<span class="cn-ordered-list-field__text">
|
|
88
|
+
<span class="cn-ordered-list-field__name">{{ option.label }}</span>
|
|
89
|
+
<span v-if="option.description" class="cn-ordered-list-field__description">{{ option.description }}</span>
|
|
90
|
+
</span>
|
|
91
|
+
</span>
|
|
92
|
+
</template>
|
|
93
|
+
</NcSelect>
|
|
94
|
+
</div>
|
|
95
|
+
<p class="hidden-visually" aria-live="polite" data-testid="cn-ordered-list-live">
|
|
96
|
+
{{ announcement }}
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
</template>
|
|
100
|
+
|
|
101
|
+
<script>
|
|
102
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
103
|
+
import { NcSelect } from '@nextcloud/vue'
|
|
104
|
+
import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
|
|
105
|
+
import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
|
|
106
|
+
import Close from 'vue-material-design-icons/Close.vue'
|
|
107
|
+
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
108
|
+
|
|
109
|
+
let orderedListUid = 0
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* CnOrderedListField - an ordered list a user keeps for themselves: rows in
|
|
113
|
+
* their order with a move up, a move down and a remove button each, and a
|
|
114
|
+
* search-as-you-type picker under it that adds a row (board
|
|
115
|
+
* DqPersoonlijkeInstellingen, "Zaaktypen in mijn menu").
|
|
116
|
+
*
|
|
117
|
+
* The app supplies the choices (`options`) and keeps the value: every change
|
|
118
|
+
* emits `update:modelValue` with the new list of values. With `searchable`
|
|
119
|
+
* the picker emits `search` as the user types, so the app can fetch matching
|
|
120
|
+
* choices itself; otherwise it filters `options`. Every move is announced in a
|
|
121
|
+
* polite live region, so reordering works with the keyboard and a screen
|
|
122
|
+
* reader, not by dragging.
|
|
123
|
+
*
|
|
124
|
+
* ```vue
|
|
125
|
+
* <CnOrderedListField
|
|
126
|
+
* v-model="menuCaseTypes"
|
|
127
|
+
* label="Case types in my menu"
|
|
128
|
+
* add-label="Add case type"
|
|
129
|
+
* :options="caseTypes" />
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @spec openspec/changes/ordered-list-field/specs/ordered-list-field/spec.md#requirement-a-user-keeps-an-ordered-list-with-the-keyboard
|
|
133
|
+
*/
|
|
134
|
+
export default {
|
|
135
|
+
name: 'CnOrderedListField',
|
|
136
|
+
|
|
137
|
+
components: { ChevronDown, ChevronUp, Close, CnIcon, NcSelect },
|
|
138
|
+
|
|
139
|
+
props: {
|
|
140
|
+
/** The values in their order. */
|
|
141
|
+
modelValue: {
|
|
142
|
+
type: Array,
|
|
143
|
+
default: () => [],
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The choices, `{ value, label, description?, icon? }` (a plain string
|
|
148
|
+
* is its own label). `description` is a muted second line ("19 open
|
|
149
|
+
* cases"), in the row and in the picker; `icon` an MDI name drawn in a
|
|
150
|
+
* small tile before the label. A value in `modelValue` without a
|
|
151
|
+
* choice shows the value itself.
|
|
152
|
+
*/
|
|
153
|
+
options: {
|
|
154
|
+
type: Array,
|
|
155
|
+
default: () => [],
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/** The list's label, also its accessible name. */
|
|
159
|
+
label: {
|
|
160
|
+
type: String,
|
|
161
|
+
default: '',
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
/** The picker's label. */
|
|
165
|
+
addLabel: {
|
|
166
|
+
type: String,
|
|
167
|
+
default: '',
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/** Placeholder in the picker. */
|
|
171
|
+
addPlaceholder: {
|
|
172
|
+
type: String,
|
|
173
|
+
default: '',
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
/** Shown when the list is empty. */
|
|
177
|
+
emptyText: {
|
|
178
|
+
type: String,
|
|
179
|
+
default: '',
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/** Whether the app searches (`search` event) instead of the picker filtering `options`. */
|
|
183
|
+
searchable: {
|
|
184
|
+
type: Boolean,
|
|
185
|
+
default: false,
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/** Whether the app is fetching choices. */
|
|
189
|
+
loading: {
|
|
190
|
+
type: Boolean,
|
|
191
|
+
default: false,
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/** A read-only list: no buttons and no picker. */
|
|
195
|
+
disabled: {
|
|
196
|
+
type: Boolean,
|
|
197
|
+
default: false,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
emits: ['update:modelValue', 'search'],
|
|
202
|
+
|
|
203
|
+
data() {
|
|
204
|
+
orderedListUid += 1
|
|
205
|
+
return {
|
|
206
|
+
labelId: `cn-ordered-list-label-${orderedListUid}`,
|
|
207
|
+
announcement: '',
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
computed: {
|
|
212
|
+
/** @return {Array<{value: string, label: string}>} The choices in one shape. */
|
|
213
|
+
choices() {
|
|
214
|
+
return (Array.isArray(this.options) ? this.options : [])
|
|
215
|
+
.map((option) => (option && typeof option === 'object'
|
|
216
|
+
? {
|
|
217
|
+
value: String(option.value ?? option.id ?? ''),
|
|
218
|
+
label: String(option.label ?? option.title ?? option.value ?? ''),
|
|
219
|
+
description: typeof option.description === 'string' ? option.description : '',
|
|
220
|
+
icon: typeof option.icon === 'string' ? option.icon : '',
|
|
221
|
+
}
|
|
222
|
+
: { value: String(option), label: String(option), description: '', icon: '' }))
|
|
223
|
+
.filter((option) => option.value !== '')
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
/** @return {Array<{value: string, label: string, description: string, icon: string}>} The rows, in order. */
|
|
227
|
+
rows() {
|
|
228
|
+
return (Array.isArray(this.modelValue) ? this.modelValue : []).map((value) => {
|
|
229
|
+
const key = String(value)
|
|
230
|
+
const choice = this.choices.find((option) => option.value === key)
|
|
231
|
+
return choice || { value: key, label: key, description: '', icon: '' }
|
|
232
|
+
})
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
/** @return {Array<{value: string, label: string}>} The choices not in the list yet. */
|
|
236
|
+
available() {
|
|
237
|
+
const taken = new Set(this.rows.map((row) => row.value))
|
|
238
|
+
return this.choices.filter((option) => !taken.has(option.value))
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
/** @return {string} The list's accessible name without a label. */
|
|
242
|
+
listLabel() {
|
|
243
|
+
return t('nextcloud-vue', 'Ordered list')
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
methods: {
|
|
248
|
+
t,
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The id of a row's description, which its buttons name as their description.
|
|
252
|
+
*
|
|
253
|
+
* @param {{value: string}} row The row.
|
|
254
|
+
* @return {string}
|
|
255
|
+
*/
|
|
256
|
+
descriptionId(row) {
|
|
257
|
+
return `${this.labelId}-desc-${String(row.value).replace(/[^\w-]/g, '-')}`
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Emit the new order.
|
|
262
|
+
*
|
|
263
|
+
* @param {Array<string>} values The values.
|
|
264
|
+
*/
|
|
265
|
+
emitValue(values) {
|
|
266
|
+
/**
|
|
267
|
+
* @event update:modelValue The list changed. Payload: the values in their order.
|
|
268
|
+
*/
|
|
269
|
+
this.$emit('update:modelValue', values)
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Move a row to another place and keep focus on the pressed button
|
|
274
|
+
* where it still makes sense.
|
|
275
|
+
*
|
|
276
|
+
* @param {number} from Its index.
|
|
277
|
+
* @param {number} to The index it moves to.
|
|
278
|
+
*/
|
|
279
|
+
move(from, to) {
|
|
280
|
+
if (to < 0 || to >= this.rows.length) {
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
const values = this.rows.map((row) => row.value)
|
|
284
|
+
const [value] = values.splice(from, 1)
|
|
285
|
+
values.splice(to, 0, value)
|
|
286
|
+
const row = this.rows[from]
|
|
287
|
+
this.emitValue(values)
|
|
288
|
+
this.announcement = t('nextcloud-vue', '{item} moved to position {position} of {total}', { item: row.label, position: to + 1, total: values.length })
|
|
289
|
+
this.$nextTick(() => {
|
|
290
|
+
const direction = to < from ? 'up' : 'down'
|
|
291
|
+
const atEdge = (direction === 'up' && to === 0) || (direction === 'down' && to === values.length - 1)
|
|
292
|
+
const target = this.$el.querySelector(`[data-testid="cn-ordered-list-${atEdge ? (direction === 'up' ? 'down' : 'up') : direction}-${row.value}"]`)
|
|
293
|
+
if (target && !target.disabled) {
|
|
294
|
+
target.focus()
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Remove a row.
|
|
301
|
+
*
|
|
302
|
+
* @param {number} index Its index.
|
|
303
|
+
*/
|
|
304
|
+
remove(index) {
|
|
305
|
+
const row = this.rows[index]
|
|
306
|
+
this.emitValue(this.rows.filter((_, i) => i !== index).map((r) => r.value))
|
|
307
|
+
this.announcement = t('nextcloud-vue', '{item} removed', { item: row.label })
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Add the picked choice at the end.
|
|
312
|
+
*
|
|
313
|
+
* @param {{value: string, label: string}|null} option The choice.
|
|
314
|
+
*/
|
|
315
|
+
add(option) {
|
|
316
|
+
if (!option) {
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
this.emitValue([...this.rows.map((row) => row.value), option.value])
|
|
320
|
+
this.announcement = t('nextcloud-vue', '{item} added', { item: option.label })
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Pass the typed text on when the app searches.
|
|
325
|
+
*
|
|
326
|
+
* @param {string} query The text.
|
|
327
|
+
*/
|
|
328
|
+
onSearch(query) {
|
|
329
|
+
if (this.searchable) {
|
|
330
|
+
/**
|
|
331
|
+
* @event search The user typed in the picker (with `searchable`). Payload: the text.
|
|
332
|
+
*/
|
|
333
|
+
this.$emit('search', query)
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
</script>
|
|
339
|
+
|
|
340
|
+
<style scoped>
|
|
341
|
+
.cn-ordered-list-field {
|
|
342
|
+
display: flex;
|
|
343
|
+
flex-direction: column;
|
|
344
|
+
gap: 10px;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.cn-ordered-list-field__label {
|
|
348
|
+
margin: 0;
|
|
349
|
+
font-weight: 600;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.cn-ordered-list-field__rows {
|
|
353
|
+
display: flex;
|
|
354
|
+
flex-direction: column;
|
|
355
|
+
margin: 0;
|
|
356
|
+
padding: 0;
|
|
357
|
+
border: 1px solid var(--color-border);
|
|
358
|
+
border-radius: var(--border-radius-element, 8px);
|
|
359
|
+
list-style: none;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.cn-ordered-list-field__row {
|
|
363
|
+
display: flex;
|
|
364
|
+
align-items: center;
|
|
365
|
+
gap: 10px;
|
|
366
|
+
min-height: 44px;
|
|
367
|
+
padding: 2px 6px 2px 12px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.cn-ordered-list-field__row + .cn-ordered-list-field__row {
|
|
371
|
+
border-top: 1px solid var(--color-border);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.cn-ordered-list-field__position {
|
|
375
|
+
flex: none;
|
|
376
|
+
min-width: 18px;
|
|
377
|
+
color: var(--color-text-maxcontrast);
|
|
378
|
+
font-size: 13px;
|
|
379
|
+
font-variant-numeric: tabular-nums;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.cn-ordered-list-field__text {
|
|
383
|
+
flex: 1 1 auto;
|
|
384
|
+
min-width: 0;
|
|
385
|
+
overflow-wrap: anywhere;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.cn-ordered-list-field__actions {
|
|
389
|
+
display: flex;
|
|
390
|
+
flex: none;
|
|
391
|
+
gap: 2px;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.cn-ordered-list-field__button {
|
|
395
|
+
display: inline-flex;
|
|
396
|
+
align-items: center;
|
|
397
|
+
justify-content: center;
|
|
398
|
+
width: 40px;
|
|
399
|
+
min-width: 40px;
|
|
400
|
+
height: 40px;
|
|
401
|
+
min-height: 40px;
|
|
402
|
+
margin: 0;
|
|
403
|
+
padding: 0;
|
|
404
|
+
border: 0;
|
|
405
|
+
border-radius: var(--border-radius-element, 8px);
|
|
406
|
+
background: transparent;
|
|
407
|
+
color: var(--color-main-text);
|
|
408
|
+
cursor: pointer;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.cn-ordered-list-field__button:hover:not(:disabled) {
|
|
412
|
+
background: var(--color-background-hover);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.cn-ordered-list-field__button:focus-visible {
|
|
416
|
+
outline: 2px solid var(--color-primary-element);
|
|
417
|
+
outline-offset: 1px;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.cn-ordered-list-field__button:disabled {
|
|
421
|
+
opacity: 0.4;
|
|
422
|
+
cursor: default;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.cn-ordered-list-field__empty {
|
|
426
|
+
margin: 0;
|
|
427
|
+
color: var(--color-text-maxcontrast);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.cn-ordered-list-field__tile {
|
|
431
|
+
display: inline-flex;
|
|
432
|
+
flex: none;
|
|
433
|
+
align-items: center;
|
|
434
|
+
justify-content: center;
|
|
435
|
+
width: 32px;
|
|
436
|
+
height: 32px;
|
|
437
|
+
border-radius: var(--border-radius-element, 8px);
|
|
438
|
+
background: var(--color-background-dark);
|
|
439
|
+
color: var(--color-main-text);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.cn-ordered-list-field__name {
|
|
443
|
+
display: block;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.cn-ordered-list-field__description {
|
|
447
|
+
display: block;
|
|
448
|
+
color: var(--color-text-maxcontrast);
|
|
449
|
+
font-size: 13px;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.cn-ordered-list-field__option {
|
|
453
|
+
display: flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
gap: 10px;
|
|
456
|
+
min-width: 0;
|
|
457
|
+
}
|
|
458
|
+
</style>
|