@conduction/nextcloud-vue 2.102.0 → 2.104.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/CnActionsBar/CnActionsBar.vue2.js +23 -0
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +94 -72
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +18 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -3
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +73 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +99 -64
- 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 +46 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +37 -2
- 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 +29 -6
- 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 +39 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +14 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +40 -2
- 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/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +113 -20
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +387 -359
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +173 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +107 -87
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +7 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +10 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue.js +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js +57 -5
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js +37 -4
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +97 -3
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +46 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +9 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js.map +1 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js +165 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js +85 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -0
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +54 -6
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +3 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +8 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +18 -2
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +7 -7
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +4 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +4 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/mixins/pageViews.js +8 -1
- package/dist/esm/mixins/pageViews.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +586 -416
- package/dist/esm/utils/nextcloudDirectory.js +18 -1
- package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
- package/dist/esm/utils/sortOrder.js +61 -0
- package/dist/esm/utils/sortOrder.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/visibleWhen.js +13 -2
- package/dist/esm/utils/visibleWhen.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +12270 -10936
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +586 -416
- package/l10n/en.json +4 -0
- package/l10n/nl.json +4 -0
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.md +1 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +39 -2
- package/src/components/CnAppRoot/CnAppRoot.md +1 -1
- package/src/components/CnAppRoot/CnAppRoot.vue +21 -4
- package/src/components/CnConversationThread/CnConversationThread.md +17 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +113 -8
- package/src/components/CnConversationThread/CnConversationWidget.vue +60 -1
- package/src/components/CnDashboardPage/CnDashboardPage.md +4 -1
- package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -6
- package/src/components/CnDataTable/CnDataTable.md +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +49 -0
- package/src/components/CnDetailPage/CnDetailPage.md +2 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +40 -2
- package/src/components/CnFormDialog/CnFormDialog.md +1 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +127 -0
- package/src/components/CnIndexPage/CnIndexPage.md +14 -2
- package/src/components/CnIndexPage/CnIndexPage.vue +192 -11
- package/src/components/CnIndexPage/useSelfFetchList.js +6 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +10 -1
- package/src/components/CnObjectRow/CnObjectRow.vue +95 -1
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +134 -2
- package/src/components/CnSearchSelect/CnSearchSelect.vue +283 -0
- package/src/components/CnSearchSelect/index.js +12 -0
- package/src/components/CnSetupWizard/CnSetupWizard.vue +56 -6
- package/src/components/CnTabsWidget/CnTabsWidget.vue +8 -1
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +7 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +18 -2
- package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
- package/src/components/index.js +1 -0
- package/src/composables/indexSources.js +7 -7
- package/src/css/dialog.css +5 -3
- package/src/css/index-page.css +19 -0
- package/src/css/look-board-index.css +48 -0
- package/src/index.js +1 -0
- package/src/mixins/pageViews.js +8 -1
- package/src/schemas/app-manifest-v2.schema.json +75 -6
- package/src/utils/nextcloudDirectory.js +17 -0
- package/src/utils/sortOrder.js +58 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/visibleWhen.js +13 -2
|
@@ -368,6 +368,20 @@ export default {
|
|
|
368
368
|
default: 0,
|
|
369
369
|
},
|
|
370
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Rows per page: the board's pager under the table ("4 of 7", page
|
|
373
|
+
* buttons, Next), forwarded to CnDataTable. It pages the rows the
|
|
374
|
+
* widget holds; with a `source`, `source.limit` is how many are read
|
|
375
|
+
* (OpenRegister's default page otherwise). 0 (the default) shows no
|
|
376
|
+
* pager.
|
|
377
|
+
*
|
|
378
|
+
* @spec openspec/changes/object-table-pager/specs/object-table/spec.md#requirement-an-object-table-can-page-its-rows
|
|
379
|
+
*/
|
|
380
|
+
pageSize: {
|
|
381
|
+
type: Number,
|
|
382
|
+
default: 0,
|
|
383
|
+
},
|
|
384
|
+
|
|
371
385
|
/**
|
|
372
386
|
* vue-router route object for the "View all" footer link. Forwarded
|
|
373
387
|
* to CnDataTable (footer shows when the rows are a `limit`-ed subset).
|
|
@@ -611,7 +625,9 @@ export default {
|
|
|
611
625
|
params[`_order[${field}]`] = dir === 'desc' ? 'desc' : 'asc'
|
|
612
626
|
}
|
|
613
627
|
if (typeof src.limit === 'number' && src.limit > 0) {
|
|
614
|
-
|
|
628
|
+
// Under a pager `limit` is how many rows the pages hold, not a
|
|
629
|
+
// "View all" cap that needs to see one more.
|
|
630
|
+
params._limit = this.pageSize > 0 ? src.limit : src.limit + 1
|
|
615
631
|
}
|
|
616
632
|
// `source.extend` (#91 Wave 3): forwarded as OR's repeated
|
|
617
633
|
// `_extend[]` param so virtual/declarative fields (e.g.
|
|
@@ -736,7 +752,7 @@ export default {
|
|
|
736
752
|
inner.register = this.source.register
|
|
737
753
|
inner.schemaId = this.source.schema
|
|
738
754
|
inner.fetchParams = this.resolvedFetchParams
|
|
739
|
-
if (typeof this.source.limit === 'number' && this.source.limit > 0) {
|
|
755
|
+
if (typeof this.source.limit === 'number' && this.source.limit > 0 && !(this.pageSize > 0)) {
|
|
740
756
|
inner.limit = this.source.limit
|
|
741
757
|
}
|
|
742
758
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
defaultWidth="wizard"
|
|
6
6
|
:eyebrow="boardEyebrow"
|
|
7
7
|
:subtitle="subtitle"
|
|
8
|
-
:name="
|
|
8
|
+
:name="shownTitle"
|
|
9
9
|
size="large"
|
|
10
10
|
:open="dialogOpen"
|
|
11
11
|
:noClose="loading || !cancellable"
|
|
@@ -265,6 +265,12 @@ export default {
|
|
|
265
265
|
* Empty uses the dialog title.
|
|
266
266
|
*/
|
|
267
267
|
eyebrowContext: { type: String, default: '' },
|
|
268
|
+
/**
|
|
269
|
+
* Board look: head the dialog with the current step's label, the
|
|
270
|
+
* dialog title moving to the eyebrow's context. The result phase and
|
|
271
|
+
* the Nextcloud look keep `dialogTitle`.
|
|
272
|
+
*/
|
|
273
|
+
stepHeading: { type: Boolean, default: false },
|
|
268
274
|
/**
|
|
269
275
|
* The eyebrow sentence, with `{context}`, `{step}` and `{total}`
|
|
270
276
|
* placeholders, so it can be translated.
|
|
@@ -356,6 +362,19 @@ export default {
|
|
|
356
362
|
.replace('{total}', String(this.steps.length))
|
|
357
363
|
},
|
|
358
364
|
|
|
365
|
+
/**
|
|
366
|
+
* The dialog's heading: the current step's label with `stepHeading`
|
|
367
|
+
* in the board look, otherwise `dialogTitle`.
|
|
368
|
+
*
|
|
369
|
+
* @return {string} The heading.
|
|
370
|
+
*/
|
|
371
|
+
shownTitle() {
|
|
372
|
+
if (this.stepHeading && this.isBoardLook && this.result === null && this.currentStep && this.currentStep.label) {
|
|
373
|
+
return this.currentStep.label
|
|
374
|
+
}
|
|
375
|
+
return this.dialogTitle
|
|
376
|
+
},
|
|
377
|
+
|
|
359
378
|
/** @return {object} The check icon, the default finishing icon. */
|
|
360
379
|
Check() {
|
|
361
380
|
return Check
|
package/src/components/index.js
CHANGED
|
@@ -130,6 +130,7 @@ export { default as CnContactsTab } from '../integrations/builtin/contacts/CnCon
|
|
|
130
130
|
export { CnContactPicker } from './CnContactPicker/index.js'
|
|
131
131
|
export { CnContactCreate } from './CnContactCreate/index.js'
|
|
132
132
|
export { CnResourceSelect } from './CnResourceSelect/index.js'
|
|
133
|
+
export { CnSearchSelect } from './CnSearchSelect/index.js'
|
|
133
134
|
export { CnPropertySourceField } from './CnPropertySourceField/index.js'
|
|
134
135
|
export { CnDurationField } from './CnDurationField/index.js'
|
|
135
136
|
export { CnFavouriteToggle } from './CnFavouriteToggle/index.js'
|
|
@@ -14,7 +14,7 @@ import { formatRelativeDate } from '../utils/boardDate.js'
|
|
|
14
14
|
import { fetchReferenceObjects } from '../utils/boardReferenceColumns.js'
|
|
15
15
|
import { fillTemplate } from '../utils/fillTemplate.js'
|
|
16
16
|
import { FLOW_RUN_ERROR_STATUSES, flowRunStatuses } from '../utils/flowRunStatus.js'
|
|
17
|
-
import { fetchGroupMembers, fetchPresence, fetchStatuses } from '../utils/nextcloudDirectory.js'
|
|
17
|
+
import { fetchGroupMembers, fetchPresence, fetchStatuses, fetchUsers } from '../utils/nextcloudDirectory.js'
|
|
18
18
|
import { readPath } from '../utils/readPath.js'
|
|
19
19
|
import { useFlowStore } from './useFlowStore.js'
|
|
20
20
|
import { organisationRecord, useOrganisationStore } from './useOrganisationStore.js'
|
|
@@ -1054,7 +1054,8 @@ function searchSource(config = {}, context = {}) {
|
|
|
1054
1054
|
* DqAfdelingen). Pair it with `folderSidebar: { source: 'groups',
|
|
1055
1055
|
* filterField: 'group' }`, which lists the groups as the folder tree; the
|
|
1056
1056
|
* selected group arrives as `group`. `sourceConfig.group` is the group shown
|
|
1057
|
-
* before one is picked
|
|
1057
|
+
* before one is picked; without one, All lists every user the reader may
|
|
1058
|
+
* list (all users for an admin, their groups' users for a sub-admin).
|
|
1058
1059
|
*
|
|
1059
1060
|
* Today is the dav app's out-of-office period when one runs now ("Absent
|
|
1060
1061
|
* until 14 Oct"), else the user status (present, away) and its message. The
|
|
@@ -1088,14 +1089,13 @@ function usersSource() {
|
|
|
1088
1089
|
load: async (config) => {
|
|
1089
1090
|
const group = String((config && (config.group || config.defaultGroup)) || '')
|
|
1090
1091
|
const mine = ++seq
|
|
1091
|
-
if (group === '') {
|
|
1092
|
-
state.rows = []
|
|
1093
|
-
return
|
|
1094
|
-
}
|
|
1095
1092
|
state.loading = true
|
|
1096
1093
|
try {
|
|
1097
1094
|
const { default: axios } = await import('@nextcloud/axios')
|
|
1098
|
-
const [members, statuses] = await Promise.all([
|
|
1095
|
+
const [members, statuses] = await Promise.all([
|
|
1096
|
+
group === '' ? fetchUsers(axios, { limit: (config && config.limit) || 500 }) : fetchGroupMembers(axios, group),
|
|
1097
|
+
fetchStatuses(axios),
|
|
1098
|
+
])
|
|
1099
1099
|
const functionField = (config && config.functionField) || 'headline'
|
|
1100
1100
|
const rolesField = (config && config.rolesField) || 'role'
|
|
1101
1101
|
const rows = await Promise.all(members.map(async (user) => {
|
package/src/css/dialog.css
CHANGED
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
* Theme hooks (all optional, defaults below):
|
|
10
10
|
* --cn-dialog-danger fill of a destructive primary
|
|
11
11
|
* --cn-dialog-eyebrow-color eyebrow colour
|
|
12
|
-
* --cn-dialog-eyebrow-transform
|
|
12
|
+
* --cn-dialog-eyebrow-transform none (default, sentence case) or uppercase
|
|
13
|
+
* --cn-dialog-eyebrow-tracking letter spacing; 0 by default, 0.06em suits capitals
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
.cn-dialog-board {
|
|
16
17
|
--cn-dialog-width: 640px;
|
|
17
18
|
--cn-dialog-danger: var(--color-error);
|
|
18
19
|
--cn-dialog-eyebrow-color: var(--color-primary-element-text, var(--color-main-text));
|
|
19
|
-
--cn-dialog-eyebrow-transform:
|
|
20
|
+
--cn-dialog-eyebrow-transform: none;
|
|
21
|
+
--cn-dialog-eyebrow-tracking: 0;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/* Dimmed page behind the dialog: text colour at 45%. */
|
|
@@ -73,7 +75,7 @@
|
|
|
73
75
|
margin-bottom: 4px;
|
|
74
76
|
font-size: 13px;
|
|
75
77
|
font-weight: 700;
|
|
76
|
-
letter-spacing: 0.06em;
|
|
78
|
+
letter-spacing: var(--cn-dialog-eyebrow-tracking, 0.06em);
|
|
77
79
|
text-transform: var(--cn-dialog-eyebrow-transform, uppercase);
|
|
78
80
|
color: var(--cn-dialog-eyebrow-color, var(--color-main-text));
|
|
79
81
|
}
|
package/src/css/index-page.css
CHANGED
|
@@ -190,6 +190,25 @@
|
|
|
190
190
|
font-size: 14px;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/* The selected folder's heading above the list. */
|
|
194
|
+
.cn-index-page__scope-heading {
|
|
195
|
+
display: flex;
|
|
196
|
+
flex-direction: column;
|
|
197
|
+
gap: 4px;
|
|
198
|
+
margin-bottom: 12px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.cn-index-page__scope-title {
|
|
202
|
+
margin: 0;
|
|
203
|
+
font-size: 1.25em;
|
|
204
|
+
font-weight: 700;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.cn-index-page__scope-subtitle {
|
|
208
|
+
font-size: 14px;
|
|
209
|
+
color: var(--color-text-maxcontrast);
|
|
210
|
+
}
|
|
211
|
+
|
|
193
212
|
/* Opt-in folder navigation pane (left of the list). */
|
|
194
213
|
.cn-index-page__folder-pane {
|
|
195
214
|
flex: 0 0 240px;
|
|
@@ -372,6 +372,17 @@
|
|
|
372
372
|
cursor: not-allowed;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
/* `searchPlacement: "toolbar"`: the search field on row 1 takes the room
|
|
376
|
+
before Filter and the view switch, which keep the end of the row. */
|
|
377
|
+
.cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--views > .cn-actions-bar__search--toolbar {
|
|
378
|
+
flex: 1 1 220px;
|
|
379
|
+
min-width: 180px;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--views > .cn-actions-bar__search--toolbar ~ .cn-actions-bar__view-controls {
|
|
383
|
+
margin-inline-start: 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
375
386
|
/* The search field: 40px, radius 8, not a pill. */
|
|
376
387
|
.cn-look-board .cn-actions-bar--board .cn-actions-bar__search {
|
|
377
388
|
flex: 1 1 260px;
|
|
@@ -1108,3 +1119,40 @@
|
|
|
1108
1119
|
.cn-look-board:has(.cn-index-page) > .app-sidebar__toggle {
|
|
1109
1120
|
display: none;
|
|
1110
1121
|
}
|
|
1122
|
+
|
|
1123
|
+
/* The folder pane as a card (DqAfdelingen): 280px, a hairline border, 12px
|
|
1124
|
+
corners, with each folder's count in a grey pill. */
|
|
1125
|
+
.cn-look-board .cn-index-page__folder-pane {
|
|
1126
|
+
flex: 0 0 280px;
|
|
1127
|
+
align-self: flex-start;
|
|
1128
|
+
margin-inline-end: 20px;
|
|
1129
|
+
padding: 14px;
|
|
1130
|
+
border: 1px solid var(--cn-board-hairline);
|
|
1131
|
+
border-radius: var(--cn-board-card-radius, 12px);
|
|
1132
|
+
background: var(--color-main-background);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
.cn-look-board .cn-index-page__folder-pane .cn-folder-tree__count {
|
|
1136
|
+
display: inline-flex;
|
|
1137
|
+
align-items: center;
|
|
1138
|
+
justify-content: center;
|
|
1139
|
+
min-width: 20px;
|
|
1140
|
+
height: 20px;
|
|
1141
|
+
padding: 0 6px;
|
|
1142
|
+
box-sizing: border-box;
|
|
1143
|
+
border-radius: 10px;
|
|
1144
|
+
background: var(--color-background-dark);
|
|
1145
|
+
color: var(--color-main-text);
|
|
1146
|
+
font-size: 12px;
|
|
1147
|
+
font-weight: 600;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/* The selected folder's heading above the list: a 20px title over a soft line. */
|
|
1151
|
+
.cn-look-board .cn-index-page__scope-heading {
|
|
1152
|
+
margin-bottom: 16px;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
.cn-look-board .cn-index-page__scope-title {
|
|
1156
|
+
font-size: 20px;
|
|
1157
|
+
font-weight: 700;
|
|
1158
|
+
}
|
package/src/index.js
CHANGED
package/src/mixins/pageViews.js
CHANGED
|
@@ -270,12 +270,14 @@ export const pageViews = {
|
|
|
270
270
|
|
|
271
271
|
/**
|
|
272
272
|
* Whether a user arranges the views for themselves (`userLayout`).
|
|
273
|
+
* Not while Buildiq edits the page (`editingBody`): Buildiq is the
|
|
274
|
+
* layout editor, and it shows and changes the manifest layout.
|
|
273
275
|
*
|
|
274
276
|
* @return {boolean}
|
|
275
277
|
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
276
278
|
*/
|
|
277
279
|
viewUserLayoutOn() {
|
|
278
|
-
return this.userLayout === true && this.hasViews
|
|
280
|
+
return this.userLayout === true && this.hasViews && this.editingBody !== true
|
|
279
281
|
},
|
|
280
282
|
|
|
281
283
|
/**
|
|
@@ -640,6 +642,11 @@ export const pageViews = {
|
|
|
640
642
|
this.viewUserLayouts = { ...this.viewUserLayouts, [view.id]: arranged }
|
|
641
643
|
this.viewUserLayoutsDirty = { ...this.viewUserLayoutsDirty, [view.id]: true }
|
|
642
644
|
this.$emit('view-layout-change', { view: view.id, layout: arranged })
|
|
645
|
+
// Outside an in-page edit session (the dashboard's edit mode, the
|
|
646
|
+
// detail page's Arrange) no Done will come to save it.
|
|
647
|
+
if (this.isEditing !== true && this.arrangingView !== true) {
|
|
648
|
+
this.saveViewUserLayouts()
|
|
649
|
+
}
|
|
643
650
|
return
|
|
644
651
|
}
|
|
645
652
|
for (const u of updated) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
|
|
4
4
|
"title": "Conduction App Manifest v2",
|
|
5
5
|
"description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.127.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -115,10 +115,11 @@
|
|
|
115
115
|
}
|
|
116
116
|
},
|
|
117
117
|
"optionsSource": { "type": "string", "description": "choice: where to read the option list from at runtime, instead of declaring it here. Names a key in the app's own GET /api/setup/status document (dots walk into nested objects, e.g. `profiles`). Entries accept `id` for `value` and `name` for `label`, and a numeric `objectCount` becomes the stat a card shows. Use it when the SERVER owns what is on offer, so the manifest cannot drift from it." },
|
|
118
|
-
"display": { "type": "string", "enum": ["select", "cards"], "default": "select", "description": "choice: how the options are rendered. `select` is a dropdown; `cards` is a grid of CnChoiceCards, which shows each option's description and stats. Pick cards when the choice needs explaining." },
|
|
118
|
+
"display": { "type": "string", "enum": ["select", "cards", "rows"], "default": "select", "description": "choice: how the options are rendered. `select` is a dropdown; `cards` is a grid of CnChoiceCards, which shows each option's description and stats; `rows` is CnChoiceCards' rows layout, one radio row per option with its description below the label. Pick cards or rows when the choice needs explaining." },
|
|
119
119
|
"dependsOn": { "type": "string", "description": "choice: the configKey of a previous choice step this one cascades from. Disabled until the parent is set; its option list is taken from optionsByParent[parentValue] (e.g. organisation-type depends on country)." },
|
|
120
120
|
"optionsByParent": { "type": "object", "description": "choice: map of parent-choice value → options[] for a dependent (dependsOn) choice. Same option item shape as `options`.", "additionalProperties": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["value", "label"], "properties": { "value": {}, "label": { "type": "string" } } } } },
|
|
121
121
|
"suggestFrom": { "type": "string", "description": "choice: the configKey of an earlier choice this step pre-fills a suggestion from. When that value is set and the user has not yet picked here, the option matching suggestMap[parentValue] is auto-selected (still user-overridable)." },
|
|
122
|
+
"default": { "description": "choice: the option value chosen when the step opens (an array of values with `multiple`). Not applied to a step the server already completed." },
|
|
122
123
|
"suggestMap": { "type": "object", "description": "choice: map of suggestFrom value → suggested option value. Used to auto-suggest a default (e.g. RGS template suggested from organisation type).", "additionalProperties": { "type": "string" } },
|
|
123
124
|
"multiple": { "type": "boolean", "default": false, "description": "choice: allow multi-select." },
|
|
124
125
|
"loadAction": { "type": "string", "minLength": 1, "description": "choice with display cards: an actionId each option card loads itself with. Every card except the one whose value is `none` gets its own Load button, which POSTs { dataset: <value> } to /api/setup/action/{loadAction}, spins while it runs and shows the result on that card. A successful load also selects the card. With loadAction the separate run-action step that loaded the picked dataset is no longer needed; manifests that keep it still work." },
|
|
@@ -541,6 +542,18 @@
|
|
|
541
542
|
}
|
|
542
543
|
},
|
|
543
544
|
"$defs": {
|
|
545
|
+
"fieldOverride": {
|
|
546
|
+
"type": "object",
|
|
547
|
+
"additionalProperties": true,
|
|
548
|
+
"description": "One field's overrides over its schema-derived descriptor (label, order, hidden, readOnly, widget, enumLabels and more stay free-form). A `widget` that is not one of the form's own widget names mounts the registered component of that name (the v2 registry, then customComponents). 2.126.0 documents the search look of a single select.",
|
|
549
|
+
"properties": {
|
|
550
|
+
"widget": { "type": "string", "minLength": 1 },
|
|
551
|
+
"look": { "type": "string", "enum": ["search"], "description": "`search` draws a single select as CnSearchSelect: a magnifier instead of the chevron, and rows with initials or an icon, the name, a secondary line and a check on the chosen one (DqOverdragen \"Nieuw team\")." },
|
|
552
|
+
"optionSecondary": { "type": "string", "description": "look 'search': the row's secondary line, a template filled from each option (\"{members} leden · {focus}\")." },
|
|
553
|
+
"optionAvatar": { "type": "boolean", "description": "look 'search': draw each option's initials." },
|
|
554
|
+
"optionIcon": { "type": "string", "minLength": 1, "description": "look 'search': one icon for every row." }
|
|
555
|
+
}
|
|
556
|
+
},
|
|
544
557
|
"breadcrumbCrumb": {
|
|
545
558
|
"type": "object",
|
|
546
559
|
"additionalProperties": false,
|
|
@@ -1985,6 +1998,7 @@
|
|
|
1985
1998
|
"items": { "$ref": "#/$defs/action" },
|
|
1986
1999
|
"description": "Declarative row/widget actions rendered by CnWidgetObjectTable. object-op patch/delete render per row; object-op create renders as a widget-scoped footer affordance."
|
|
1987
2000
|
},
|
|
2001
|
+
"pageSize": { "type": "integer", "minimum": 1, "description": "Rows per page: the table shows one page with the board pager under it (\"4 of 7\", DqCatalogus). Under a pager `source.limit` is how many rows are read (2.127.0)." },
|
|
1988
2002
|
"rowClass": {
|
|
1989
2003
|
"type": "array",
|
|
1990
2004
|
"description": "#91: declarative per-row CSS class rules compiled into CnDataTable's rowClass function. Each rule { when: { field, op?, value }, class } adds `class` to a row when the shared visibleWhen predicate holds against that row (field = dot-path into the row, op = eq|neq|gt|gte|lt|lte default eq, value = literal). Rules evaluate in order and every matching class is space-joined — so overdue / at-risk rows can be highlighted from the manifest. A host-supplied rowClass FUNCTION (runtime-only) still passes straight through.",
|
|
@@ -2162,6 +2176,39 @@
|
|
|
2162
2176
|
}
|
|
2163
2177
|
}
|
|
2164
2178
|
},
|
|
2179
|
+
{
|
|
2180
|
+
"description": "record-list built-in widget (CnRecordListWidget): the action under the list, `props.content.footerAction` (2.126.0). Other content keys stay free-form.",
|
|
2181
|
+
"if": {
|
|
2182
|
+
"properties": { "widgetKey": { "const": "record-list" } },
|
|
2183
|
+
"required": ["widgetKey"]
|
|
2184
|
+
},
|
|
2185
|
+
"then": {
|
|
2186
|
+
"properties": {
|
|
2187
|
+
"props": {
|
|
2188
|
+
"type": "object",
|
|
2189
|
+
"properties": {
|
|
2190
|
+
"content": {
|
|
2191
|
+
"type": "object",
|
|
2192
|
+
"properties": {
|
|
2193
|
+
"footerAction": {
|
|
2194
|
+
"type": "object",
|
|
2195
|
+
"additionalProperties": true,
|
|
2196
|
+
"required": ["label"],
|
|
2197
|
+
"description": "One action under the list, also under the empty text (DqBezwaar \"Beroep registreren\"). Its other keys are an action's (`type`, `route`, `form`, ...). While `disabledWhen` holds it is disabled and says `disabledReason` (\"Kan zodra de beslissing is verzonden\"). Label and reason go through the host's translate.",
|
|
2198
|
+
"properties": {
|
|
2199
|
+
"label": { "type": "string", "minLength": 1 },
|
|
2200
|
+
"icon": { "type": "string", "minLength": 1 },
|
|
2201
|
+
"disabledWhen": { "$ref": "#/$defs/visibleWhen" },
|
|
2202
|
+
"disabledReason": { "type": "string" }
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
},
|
|
2165
2212
|
{
|
|
2166
2213
|
"description": "audit-trail built-in widget (CnAuditTrailWidget, Wave 1): the object change-log card. All props are optional — register/schema/objectId normally arrive via the detail-page object-context merge; explicit props win.",
|
|
2167
2214
|
"if": {
|
|
@@ -2864,7 +2911,7 @@
|
|
|
2864
2911
|
},
|
|
2865
2912
|
"fieldOverrides": {
|
|
2866
2913
|
"type": "object",
|
|
2867
|
-
"additionalProperties":
|
|
2914
|
+
"additionalProperties": { "$ref": "#/$defs/fieldOverride" },
|
|
2868
2915
|
"description": "open-form: per-field overrides passed to fieldsFromSchema, keyed by field. Recognised keys include label, order, hidden, readOnly and widget."
|
|
2869
2916
|
},
|
|
2870
2917
|
"advanced": {
|
|
@@ -3201,9 +3248,9 @@
|
|
|
3201
3248
|
},
|
|
3202
3249
|
"op": {
|
|
3203
3250
|
"type": "string",
|
|
3204
|
-
"enum": ["eq", "neq", "gt", "gte", "lt", "lte", "empty", "notEmpty", "in"],
|
|
3251
|
+
"enum": ["eq", "neq", "gt", "gte", "lt", "lte", "empty", "notEmpty", "in", "anyOf"],
|
|
3205
3252
|
"default": "eq",
|
|
3206
|
-
"description": "Comparison operator (default eq). eq/neq compare loosely-normalised primitives; ordering operators coerce both sides to Number. `in` holds when the value is one of the listed `value` array (2.87.0). `empty` / `notEmpty` take no `value` and treat undefined, null, '' and an empty array alike — the one question eq cannot ask, because an unset field arrives in a shape that depends on the store and String(null) is the word 'null' rather than blank."
|
|
3253
|
+
"description": "Comparison operator (default eq). eq/neq compare loosely-normalised primitives; ordering operators coerce both sides to Number. `in` holds when the value is one of the listed `value` array (2.87.0). `anyOf` holds when the value, or any entry of a list value, is one of the listed `value` array: a record whose `dataCategories` holds \"health\" among others matches `anyOf [\"health\", \"ethnicity\"]` (2.126.0). `empty` / `notEmpty` take no `value` and treat undefined, null, '' and an empty array alike — the one question eq cannot ask, because an unset field arrives in a shape that depends on the store and String(null) is the word 'null' rather than blank."
|
|
3207
3254
|
},
|
|
3208
3255
|
"value": {
|
|
3209
3256
|
"description": "The right-hand side of the comparison, resolved through the shared @-token grammar so `@me` names the signed-in reader rather than a literal only the server knows. Omitted for `empty` / `notEmpty`."
|
|
@@ -4006,6 +4053,9 @@
|
|
|
4006
4053
|
"additionalProperties": true,
|
|
4007
4054
|
"description": "type='index': the opt-in folder navigation pane (CnFolderSidebar) rendered left of the list. Selecting a folder filters the list by `filterField` (or `field`); \"All\" clears it. Only the per-scope list-layout keys are typed here; the pane's own keys (source, register, schema, idField, nameField, folders, allLabel, title, allowCreate, ...) stay free-form. Maps to CnIndexPage `folderSidebar`.",
|
|
4008
4055
|
"properties": {
|
|
4056
|
+
"heading": { "type": "boolean", "description": "true heads the list with the selected folder's name, its parent folder and its count (always on a `groups` pane, 2.127.0)." },
|
|
4057
|
+
"countText": { "type": "string", "minLength": 1, "description": "The count in that heading, `{count}` filled in, through the app's translate; a groups pane defaults to \"{count} members\" (2.127.0)." },
|
|
4058
|
+
"pane": { "type": "boolean", "default": true, "description": "false keeps the folder scope (a deep link such as `?caseType=<uuid>` still applies the scope's look and filter) without drawing the pane (dossiq's simple profile, 2.126.0)." },
|
|
4009
4059
|
"folders": {
|
|
4010
4060
|
"type": "array",
|
|
4011
4061
|
"description": "Explicit folder list for source:'custom', and the place a folder declares its own list layout. Every other folder key stays free-form.",
|
|
@@ -4168,6 +4218,11 @@
|
|
|
4168
4218
|
]
|
|
4169
4219
|
}
|
|
4170
4220
|
},
|
|
4221
|
+
"sideColumnFrom": {
|
|
4222
|
+
"type": "string",
|
|
4223
|
+
"minLength": 1,
|
|
4224
|
+
"description": "type='detail': a field path, read like a `visibleWhen` field, whose value picks the side column and its order for this record, e.g. `@context.caseType.sideCards` (a case type that names the cards its cases show). When the value is a non-empty list of widget ids, the side column is those of `sideColumn` in that order and the rest are left out; each still follows its own `visibleWhen`. Any other value, or no value, keeps `sideColumn` as declared. Maps to CnDetailPage `sideColumnFrom`."
|
|
4225
|
+
},
|
|
4171
4226
|
"headerActions": {
|
|
4172
4227
|
"type": "array",
|
|
4173
4228
|
"description": "Page-level header actions rendered inside CnActionsBar's overflow dropdown (between the built-in Refresh action and the `#action-items` slot). On a `type:'detail'` page the same entries render in the header's Actions menu, above the Request-a-feature / Report-a-bug / Documentation trio. Same shape as the row-level `actions[]` ($ref action), but handlers receive NO row context — they are page-level. Reserved ids (`refresh`, `import`, `export`, `copy`, `delete`) are dropped at render time to avoid shadowing built-ins. See manifest-icons-and-page-actions for the dispatch semantics.",
|
|
@@ -4301,7 +4356,7 @@
|
|
|
4301
4356
|
},
|
|
4302
4357
|
"fieldOverrides": {
|
|
4303
4358
|
"type": "object",
|
|
4304
|
-
"additionalProperties":
|
|
4359
|
+
"additionalProperties": { "$ref": "#/$defs/fieldOverride" },
|
|
4305
4360
|
"description": "Per-field overrides merged over the schema-derived field descriptors in the page's built-in Add/Edit form dialog (e.g. `{ \"description\": { \"widget\": \"textarea\" } }`). Which widget a field renders as belongs to the surface, not to the schema. Maps to CnIndexPage / CnDetailPage `fieldOverrides`."
|
|
4306
4361
|
},
|
|
4307
4362
|
"createDefaults": {
|
|
@@ -4667,10 +4722,24 @@
|
|
|
4667
4722
|
"variant": { "type": "string", "enum": ["primary", "secondary"], "default": "secondary" },
|
|
4668
4723
|
"icon": { "type": "string", "minLength": 1, "description": "MDI icon name." },
|
|
4669
4724
|
"format": { "type": "string", "enum": ["csv", "excel"], "default": "csv", "description": "For `export`." },
|
|
4725
|
+
"run": { "type": "string", "minLength": 1, "description": "For `export`: the id of a `headerActions` entry the button runs instead of the export dialog. On a named or endpoint source an Export without `run` (and without a mass-export listener) is not drawn (2.127.0)." },
|
|
4670
4726
|
"items": { "type": "array", "minItems": 1, "items": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "object", "additionalProperties": false, "required": ["heading"], "properties": { "heading": { "type": "string", "minLength": 1, "description": "i18n key of a group heading." } } } ] }, "description": "For `actions-menu`: the menu's entries, in order. Each is `header-actions` (every header action), the id of one `headerActions` entry, or a built-in of the actions bar's own menu, which the bar drops while header buttons show: `import`, `export`, `refresh`, `copy-selected`, `delete-selected` (disabled until a row is selected) or `documentation` (a link to the page's documentationUrl, left out without one). An unknown name is left out. Absent: the header actions only (screens-index-header-actions-menu, 2.90.0). Entries can be grouped: `{ \"heading\": \"<i18n key>\" }` draws a group heading and `\"separator\"` a rule, each only when an entry follows it, so a group whose entries are all left out leaves no empty heading (screens-index-header-menu-headings, 2.93.0)." }
|
|
4671
4727
|
}
|
|
4672
4728
|
}
|
|
4673
4729
|
},
|
|
4730
|
+
"search": { "type": "boolean", "default": true, "description": "type='index': `false` draws no inline search in the actions bar, also in the board look, which otherwise draws one on every index page. Wins over `inlineSearch` (2.127.0)." },
|
|
4731
|
+
"searchPlacement": { "type": "string", "enum": ["", "toolbar"], "default": "", "description": "type='index', board look: where the search field sits. `toolbar` puts it on row 1 before the Filter button and the view switch instead of opening row 2 (DqTeamwachtrij, 2.127.0)." },
|
|
4732
|
+
"defaultOrder": { "description": "type='index', self-fetch: the order the page opens on when neither the address nor sortKeys gives one: `\"-createdAt\"`, `\"priority:desc,title\"`, an `_order` map or a `[{ key, order }]` list (2.127.0).", "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "object", "additionalProperties": { "type": "string", "enum": ["asc", "desc", "ASC", "DESC"] } }, { "type": "array", "items": { "type": "object", "required": ["key"], "properties": { "key": { "type": "string", "minLength": 1 }, "order": { "type": "string", "enum": ["asc", "desc"] } } } } ] },
|
|
4733
|
+
"listConfig": {
|
|
4734
|
+
"type": "object",
|
|
4735
|
+
"additionalProperties": true,
|
|
4736
|
+
"description": "type='index': the field mapping of the list view's rows (CnObjectRow): titleField, subtitleField, imageField, iconField, iconName, badgeField and the badge keys stay free-form. 2.127.0 types the row cells and the meta template (DqTeamwachtrij).",
|
|
4737
|
+
"properties": {
|
|
4738
|
+
"subtitleTemplate": { "type": "string", "minLength": 1, "description": "The meta line: `{field}` tokens filled from the row; empty parts and their separators drop." },
|
|
4739
|
+
"leading": {"type": "object", "required": ["key"], "additionalProperties": true, "properties": {"key": {"type": "string", "minLength": 1}, "widget": {"type": "string", "minLength": 1}, "widgetProps": {"type": "object", "additionalProperties": true}, "formatter": {"type": "string", "minLength": 1}, "width": {"type": "string", "minLength": 1, "description": "CSS width, lining the column up across rows."}}, "description": "One cell before the title, drawn as a table column draws it (`{ key: \"createdAt\", widget: \"age\" }`)."},
|
|
4740
|
+
"trailing": { "description": "A cell, or a list of cells, after the badges, each drawn as a table column draws it.", "anyOf": [ {"type": "object", "required": ["key"], "additionalProperties": true, "properties": {"key": {"type": "string", "minLength": 1}, "widget": {"type": "string", "minLength": 1}, "widgetProps": {"type": "object", "additionalProperties": true}, "formatter": {"type": "string", "minLength": 1}, "width": {"type": "string", "minLength": 1, "description": "CSS width, lining the column up across rows."}}}, { "type": "array", "items": {"type": "object", "required": ["key"], "additionalProperties": true, "properties": {"key": {"type": "string", "minLength": 1}, "widget": {"type": "string", "minLength": 1}, "widgetProps": {"type": "object", "additionalProperties": true}, "formatter": {"type": "string", "minLength": 1}, "width": {"type": "string", "minLength": 1, "description": "CSS width, lining the column up across rows."}}} } ] }
|
|
4741
|
+
}
|
|
4742
|
+
},
|
|
4674
4743
|
"rowTitle": {
|
|
4675
4744
|
"type": "string",
|
|
4676
4745
|
"enum": ["link", "plain"],
|
|
@@ -64,6 +64,23 @@ export async function fetchGroupMembers(axios, groupId) {
|
|
|
64
64
|
return Array.isArray(users) ? users : Object.values(users)
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Every user the reader may list, with their details: all users for an
|
|
69
|
+
* admin, the users of their groups for a sub-admin. Refused for others.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} axios The axios instance.
|
|
72
|
+
* @param {{limit?: number}} [options] How many to read.
|
|
73
|
+
* @return {Promise<Array<object>>} Nextcloud's user details, one per user.
|
|
74
|
+
*/
|
|
75
|
+
export async function fetchUsers(axios, options = {}) {
|
|
76
|
+
const res = await axios.get(generateOcsUrl('cloud/users/details'), {
|
|
77
|
+
params: { limit: options.limit || 500, format: 'json' },
|
|
78
|
+
headers: OCS_HEADERS,
|
|
79
|
+
})
|
|
80
|
+
const users = (ocsData(res) && ocsData(res).users) || {}
|
|
81
|
+
return Array.isArray(users) ? users : Object.values(users)
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
/**
|
|
68
85
|
* Whether a user is in today: an out-of-office period that runs now (the
|
|
69
86
|
* dav app's absence, Nextcloud 28 and later), else their user status.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read an order a manifest writes into the `[{ key, order }]` list a self-fetch
|
|
8
|
+
* index page sorts by. Accepted forms:
|
|
9
|
+
*
|
|
10
|
+
* - `"title"` ascending, `"-createdAt"` descending, `"priority:desc"`;
|
|
11
|
+
* - several of those, comma separated, in priority order (`"-priority,title"`);
|
|
12
|
+
* - an OpenRegister `_order` map, `{ "priority": "desc", "title": "asc" }`;
|
|
13
|
+
* - a `[{ key, order? }]` list.
|
|
14
|
+
*
|
|
15
|
+
* @param {string|object|Array|null|undefined} value The order.
|
|
16
|
+
* @return {Array<{key: string, order: 'asc'|'desc'}>} The sort keys, possibly empty.
|
|
17
|
+
*/
|
|
18
|
+
export function orderToSortKeys(value) {
|
|
19
|
+
const direction = (order) => (String(order || '').toLowerCase() === 'desc' ? 'desc' : 'asc')
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return value
|
|
22
|
+
.filter((entry) => entry && typeof entry.key === 'string' && entry.key !== '')
|
|
23
|
+
.map((entry) => ({ key: entry.key, order: direction(entry.order) }))
|
|
24
|
+
}
|
|
25
|
+
if (value && typeof value === 'object') {
|
|
26
|
+
return Object.entries(value)
|
|
27
|
+
.filter(([key]) => key !== '')
|
|
28
|
+
.map(([key, order]) => ({ key, order: direction(order) }))
|
|
29
|
+
}
|
|
30
|
+
if (typeof value !== 'string') {
|
|
31
|
+
return []
|
|
32
|
+
}
|
|
33
|
+
return value.split(',')
|
|
34
|
+
.map((part) => part.trim())
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map((part) => {
|
|
37
|
+
if (part.startsWith('-')) {
|
|
38
|
+
return { key: part.slice(1), order: 'desc' }
|
|
39
|
+
}
|
|
40
|
+
const [key, order] = part.split(':')
|
|
41
|
+
return { key, order: direction(order) }
|
|
42
|
+
})
|
|
43
|
+
.filter((entry) => entry.key !== '')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether two sort key lists order the same way.
|
|
48
|
+
*
|
|
49
|
+
* @param {Array<{key: string, order: string}>} a One list.
|
|
50
|
+
* @param {Array<{key: string, order: string}>} b The other.
|
|
51
|
+
* @return {boolean} True when they name the same keys, directions and priority.
|
|
52
|
+
*/
|
|
53
|
+
export function sameSortKeys(a, b) {
|
|
54
|
+
const left = Array.isArray(a) ? a : []
|
|
55
|
+
const right = Array.isArray(b) ? b : []
|
|
56
|
+
return left.length === right.length
|
|
57
|
+
&& left.every((entry, index) => entry.key === right[index].key && (entry.order || 'asc') === (right[index].order || 'asc'))
|
|
58
|
+
}
|