@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
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { translate as t } from '@nextcloud/l10n'
|
|
7
|
-
import { generateUrl } from '@nextcloud/router'
|
|
6
|
+
import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
|
|
7
|
+
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
|
|
8
8
|
import { reactive } from 'vue'
|
|
9
9
|
import CalendarClock from 'vue-material-design-icons/CalendarClock.vue'
|
|
10
10
|
import HandBackRight from 'vue-material-design-icons/HandBackRight.vue'
|
|
@@ -12,7 +12,10 @@ import LightningBolt from 'vue-material-design-icons/LightningBolt.vue'
|
|
|
12
12
|
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
|
13
13
|
import { formatRelativeDate } from '../utils/boardDate.js'
|
|
14
14
|
import { FLOW_RUN_ERROR_STATUSES, flowRunStatuses } from '../utils/flowRunStatus.js'
|
|
15
|
+
import { fetchGroupMembers, fetchPresence, fetchStatuses } from '../utils/nextcloudDirectory.js'
|
|
16
|
+
import { readPath } from '../utils/readPath.js'
|
|
15
17
|
import { useFlowStore } from './useFlowStore.js'
|
|
18
|
+
import { organisationRecord, useOrganisationStore } from './useOrganisationStore.js'
|
|
16
19
|
import { useTaskInboxStore } from './useTaskInboxStore.js'
|
|
17
20
|
|
|
18
21
|
/**
|
|
@@ -714,6 +717,386 @@ function endpointSource() {
|
|
|
714
717
|
}
|
|
715
718
|
}
|
|
716
719
|
|
|
720
|
+
/**
|
|
721
|
+
* Fill `{field}` placeholders in a link template from a row.
|
|
722
|
+
*
|
|
723
|
+
* @param {string} template A path such as `/apps/dossiq/cases/{id}`.
|
|
724
|
+
* @param {object} row The row.
|
|
725
|
+
* @return {string} The path.
|
|
726
|
+
*/
|
|
727
|
+
function fillRowTemplate(template, row) {
|
|
728
|
+
return String(template).replace(/\{([\w.@]+)\}/g, (match, path) => {
|
|
729
|
+
const value = readPath(row, path)
|
|
730
|
+
return value === undefined || value === null ? '' : encodeURIComponent(String(value))
|
|
731
|
+
})
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* The search source: one search term over several OpenRegister schemas and
|
|
736
|
+
* the reader's files (`entitySource: "search"`), with a quick filter per kind
|
|
737
|
+
* and its count (DqZoekresultaten).
|
|
738
|
+
*
|
|
739
|
+
* `sourceConfig.kinds` lists what is searched:
|
|
740
|
+
* `{ id, label, register, schema, titleField?, subtitleField?, statusField?, href?, limit? }`
|
|
741
|
+
* for objects (OpenRegister's `_search`, so the reader sees what they may
|
|
742
|
+
* read), or `{ id, label, files: true, limit? }` for the reader's files
|
|
743
|
+
* (Nextcloud's unified search). The term is `sourceConfig.term`, else the
|
|
744
|
+
* address's `?q=` (or `sourceConfig.termQuery`). A kind that fails shows no
|
|
745
|
+
* rows and no count; the others still answer.
|
|
746
|
+
*
|
|
747
|
+
* @param {object} [config] The page's source config.
|
|
748
|
+
* @return {object} The source adapter.
|
|
749
|
+
* @spec openspec/changes/index-search-source/specs/index-page/spec.md#requirement-an-index-page-can-search-several-schemas-and-files
|
|
750
|
+
*/
|
|
751
|
+
function searchSource(config = {}) {
|
|
752
|
+
const kinds = (Array.isArray(config && config.kinds) ? config.kinds : [])
|
|
753
|
+
.filter((kind) => kind && typeof kind.id === 'string' && kind.id !== '' && (kind.files === true || (kind.register && kind.schema)))
|
|
754
|
+
const state = reactive({ rows: [], totals: {}, loading: false, kind: 'all', term: '' })
|
|
755
|
+
let seq = 0
|
|
756
|
+
|
|
757
|
+
const searchObjects = async (axios, kind, term) => {
|
|
758
|
+
const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: kind.register, schema: kind.schema })
|
|
759
|
+
const res = await axios.get(url, { params: { _search: term, _limit: kind.limit || 20 } })
|
|
760
|
+
const body = (res && res.data) || {}
|
|
761
|
+
const results = Array.isArray(body.results) ? body.results : (Array.isArray(body) ? body : [])
|
|
762
|
+
return {
|
|
763
|
+
total: typeof body.total === 'number' ? body.total : results.length,
|
|
764
|
+
rows: results.map((object) => {
|
|
765
|
+
const id = object.id || object.uuid || (object['@self'] && object['@self'].id)
|
|
766
|
+
return {
|
|
767
|
+
id: `${kind.id}:${id}`,
|
|
768
|
+
kind: kind.id,
|
|
769
|
+
kindLabel: kind.label,
|
|
770
|
+
title: String(readPath(object, kind.titleField || 'title') ?? object.name ?? id ?? ''),
|
|
771
|
+
subtitle: kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : '',
|
|
772
|
+
status: kind.statusField ? String(readPath(object, kind.statusField) ?? '') : '',
|
|
773
|
+
href: kind.href ? generateUrl(fillRowTemplate(kind.href, { ...object, id })) : '',
|
|
774
|
+
object,
|
|
775
|
+
}
|
|
776
|
+
}),
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const searchFiles = async (axios, kind, term) => {
|
|
781
|
+
const res = await axios.get(generateOcsUrl('search/providers/files/search'), {
|
|
782
|
+
params: { term, limit: kind.limit || 20, format: 'json' },
|
|
783
|
+
headers: { 'OCS-APIRequest': 'true' },
|
|
784
|
+
})
|
|
785
|
+
const entries = (res && res.data && res.data.ocs && res.data.ocs.data && res.data.ocs.data.entries) || []
|
|
786
|
+
return {
|
|
787
|
+
total: entries.length,
|
|
788
|
+
rows: entries.map((entry, index) => ({
|
|
789
|
+
id: `${kind.id}:${entry.resourceUrl || index}`,
|
|
790
|
+
kind: kind.id,
|
|
791
|
+
kindLabel: kind.label,
|
|
792
|
+
title: entry.title || '',
|
|
793
|
+
subtitle: entry.subline || '',
|
|
794
|
+
status: '',
|
|
795
|
+
href: entry.resourceUrl || '',
|
|
796
|
+
})),
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return {
|
|
801
|
+
/**
|
|
802
|
+
* Run the search over every kind, in parallel.
|
|
803
|
+
*
|
|
804
|
+
* @param {object} cfg The page's source config with the active tab's `kind` merged in.
|
|
805
|
+
* @return {Promise<void>} Resolves when every kind answered or failed.
|
|
806
|
+
*/
|
|
807
|
+
load: async (cfg) => {
|
|
808
|
+
state.kind = cfg && typeof cfg.kind === 'string' ? cfg.kind : 'all'
|
|
809
|
+
const query = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : null
|
|
810
|
+
const term = String((cfg && cfg.term) || (query && query.get((cfg && cfg.termQuery) || 'q')) || '').trim()
|
|
811
|
+
const mine = ++seq
|
|
812
|
+
state.term = term
|
|
813
|
+
if (term === '' || kinds.length === 0) {
|
|
814
|
+
state.rows = []
|
|
815
|
+
state.totals = {}
|
|
816
|
+
return
|
|
817
|
+
}
|
|
818
|
+
state.loading = true
|
|
819
|
+
const { default: axios } = await import('@nextcloud/axios')
|
|
820
|
+
const answers = await Promise.all(kinds.map((kind) => (kind.files === true ? searchFiles(axios, kind, term) : searchObjects(axios, kind, term)).catch(() => null)))
|
|
821
|
+
if (mine !== seq) {
|
|
822
|
+
return
|
|
823
|
+
}
|
|
824
|
+
const totals = {}
|
|
825
|
+
const rows = []
|
|
826
|
+
answers.forEach((answer, index) => {
|
|
827
|
+
if (answer) {
|
|
828
|
+
totals[kinds[index].id] = answer.total
|
|
829
|
+
rows.push(...answer.rows)
|
|
830
|
+
}
|
|
831
|
+
})
|
|
832
|
+
state.totals = totals
|
|
833
|
+
state.rows = rows
|
|
834
|
+
state.loading = false
|
|
835
|
+
},
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* @return {Array<object>} The results of the active kind, or all of them.
|
|
839
|
+
*/
|
|
840
|
+
rows: () => (state.kind === 'all' ? state.rows : state.rows.filter((row) => row.kind === state.kind)),
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* @return {boolean} Whether a search is in flight.
|
|
844
|
+
*/
|
|
845
|
+
loading: () => state.loading,
|
|
846
|
+
|
|
847
|
+
columns: [
|
|
848
|
+
{ key: 'title', label: t('nextcloud-vue', 'Result'), secondary: 'subtitle' },
|
|
849
|
+
{ key: 'kindLabel', label: t('nextcloud-vue', 'Kind') },
|
|
850
|
+
{ key: 'status', label: t('nextcloud-vue', 'Status') },
|
|
851
|
+
],
|
|
852
|
+
|
|
853
|
+
quickFilters: kinds.length > 0
|
|
854
|
+
? [
|
|
855
|
+
{ label: t('nextcloud-vue', 'All'), filter: { kind: 'all' }, default: true },
|
|
856
|
+
...kinds.map((kind) => ({ label: kind.label || kind.id, filter: { kind: kind.id } })),
|
|
857
|
+
]
|
|
858
|
+
: null,
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* The number on each kind's quick filter: the total each search
|
|
862
|
+
* reported, and their sum on All.
|
|
863
|
+
*
|
|
864
|
+
* @return {Record<number, number>} The counts by tab index.
|
|
865
|
+
*/
|
|
866
|
+
tabCounts: () => {
|
|
867
|
+
const counts = { 0: Object.values(state.totals).reduce((sum, value) => sum + value, 0) }
|
|
868
|
+
kinds.forEach((kind, index) => {
|
|
869
|
+
if (typeof state.totals[kind.id] === 'number') {
|
|
870
|
+
counts[index + 1] = state.totals[kind.id]
|
|
871
|
+
}
|
|
872
|
+
})
|
|
873
|
+
return counts
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Open a result where it lives.
|
|
878
|
+
*
|
|
879
|
+
* @param {object} row The clicked row.
|
|
880
|
+
* @return {void}
|
|
881
|
+
*/
|
|
882
|
+
openRow: (row) => {
|
|
883
|
+
if (row && row.href) {
|
|
884
|
+
window.location.assign(row.href)
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* @param {object} row A row.
|
|
890
|
+
* @return {string|null} The URL the row opens, for a new tab.
|
|
891
|
+
*/
|
|
892
|
+
rowTarget: (row) => (row && row.href) || null,
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* The users source: the members of a Nextcloud group, with their function,
|
|
898
|
+
* roles, e-mail and whether they are in today (`entitySource: "users"`,
|
|
899
|
+
* DqAfdelingen). Pair it with `folderSidebar: { source: 'groups',
|
|
900
|
+
* filterField: 'group' }`, which lists the groups as the folder tree; the
|
|
901
|
+
* selected group arrives as `group`. `sourceConfig.group` is the group shown
|
|
902
|
+
* before one is picked.
|
|
903
|
+
*
|
|
904
|
+
* Today is the dav app's out-of-office period when one runs now ("Absent
|
|
905
|
+
* until 14 Oct"), else the user status (present, away) and its message. The
|
|
906
|
+
* function is the profile `headline`, the roles the profile `role`, unless
|
|
907
|
+
* `sourceConfig.functionField` / `rolesField` name other user fields.
|
|
908
|
+
* Listing a group needs an admin or the group's sub-admin; a refused request
|
|
909
|
+
* leaves the list empty.
|
|
910
|
+
*
|
|
911
|
+
* @return {object} The source adapter.
|
|
912
|
+
* @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members
|
|
913
|
+
*/
|
|
914
|
+
function usersSource() {
|
|
915
|
+
const state = reactive({ rows: [], loading: false })
|
|
916
|
+
let seq = 0
|
|
917
|
+
const presenceLabels = {
|
|
918
|
+
present: t('nextcloud-vue', 'Present'),
|
|
919
|
+
away: t('nextcloud-vue', 'Away'),
|
|
920
|
+
}
|
|
921
|
+
const presenceColors = {
|
|
922
|
+
[presenceLabels.present]: 'success',
|
|
923
|
+
[presenceLabels.away]: 'info',
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return {
|
|
927
|
+
/**
|
|
928
|
+
* Load the group's members and their presence.
|
|
929
|
+
*
|
|
930
|
+
* @param {object} config The page's source config with the picked `group`.
|
|
931
|
+
* @return {Promise<void>} Resolves when loaded.
|
|
932
|
+
*/
|
|
933
|
+
load: async (config) => {
|
|
934
|
+
const group = String((config && (config.group || config.defaultGroup)) || '')
|
|
935
|
+
const mine = ++seq
|
|
936
|
+
if (group === '') {
|
|
937
|
+
state.rows = []
|
|
938
|
+
return
|
|
939
|
+
}
|
|
940
|
+
state.loading = true
|
|
941
|
+
try {
|
|
942
|
+
const { default: axios } = await import('@nextcloud/axios')
|
|
943
|
+
const [members, statuses] = await Promise.all([fetchGroupMembers(axios, group), fetchStatuses(axios)])
|
|
944
|
+
const functionField = (config && config.functionField) || 'headline'
|
|
945
|
+
const rolesField = (config && config.rolesField) || 'role'
|
|
946
|
+
const rows = await Promise.all(members.map(async (user) => {
|
|
947
|
+
const presence = await fetchPresence(axios, user.id, statuses[user.id] || null)
|
|
948
|
+
let today = ''
|
|
949
|
+
if (presence.state === 'absent') {
|
|
950
|
+
const until = presence.until ? new Date(`${presence.until}T12:00:00`) : null
|
|
951
|
+
today = until
|
|
952
|
+
? t('nextcloud-vue', 'Absent until {date}', { date: until.toLocaleDateString(getCanonicalLocale() || undefined, { day: 'numeric', month: 'short' }) })
|
|
953
|
+
: t('nextcloud-vue', 'Absent')
|
|
954
|
+
} else if (presence.message) {
|
|
955
|
+
today = presence.message
|
|
956
|
+
} else if (presenceLabels[presence.state]) {
|
|
957
|
+
today = presenceLabels[presence.state]
|
|
958
|
+
}
|
|
959
|
+
return {
|
|
960
|
+
id: user.id,
|
|
961
|
+
displayname: user.displayname || user['display-name'] || user.id,
|
|
962
|
+
function: String(user[functionField] || ''),
|
|
963
|
+
roles: String(user[rolesField] || ''),
|
|
964
|
+
email: user.email || '',
|
|
965
|
+
today,
|
|
966
|
+
presence: presence.state,
|
|
967
|
+
}
|
|
968
|
+
}))
|
|
969
|
+
if (mine === seq) {
|
|
970
|
+
state.rows = rows.sort((a, b) => a.displayname.localeCompare(b.displayname))
|
|
971
|
+
}
|
|
972
|
+
} catch (error) {
|
|
973
|
+
if (mine === seq) {
|
|
974
|
+
state.rows = []
|
|
975
|
+
}
|
|
976
|
+
throw error
|
|
977
|
+
} finally {
|
|
978
|
+
if (mine === seq) {
|
|
979
|
+
state.loading = false
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* @return {Array<object>} The members.
|
|
986
|
+
*/
|
|
987
|
+
rows: () => state.rows,
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* @return {boolean} Whether a load is in flight.
|
|
991
|
+
*/
|
|
992
|
+
loading: () => state.loading,
|
|
993
|
+
|
|
994
|
+
columns: [
|
|
995
|
+
{ key: 'displayname', label: t('nextcloud-vue', 'Employee'), secondary: 'function' },
|
|
996
|
+
{ key: 'roles', label: t('nextcloud-vue', 'Roles') },
|
|
997
|
+
{ key: 'email', label: t('nextcloud-vue', 'E-mail') },
|
|
998
|
+
{ key: 'today', label: t('nextcloud-vue', 'Today'), widget: 'badge', widgetProps: { colorMap: presenceColors, variant: 'warning' } },
|
|
999
|
+
],
|
|
1000
|
+
|
|
1001
|
+
searchFields: {
|
|
1002
|
+
group: { param: 'group', single: true },
|
|
1003
|
+
},
|
|
1004
|
+
|
|
1005
|
+
showAdd: false,
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* The organisations source: OpenRegister organisations, which are not
|
|
1011
|
+
* register/schema objects.
|
|
1012
|
+
*
|
|
1013
|
+
* The list is the viewer's own organisations and the single read refuses an
|
|
1014
|
+
* organisation the viewer has no access to; both are decided by OpenRegister
|
|
1015
|
+
* from the session. The adapter forwards nothing that could widen that.
|
|
1016
|
+
*
|
|
1017
|
+
* It is a RECORD source as well (`loadRecord` + `record`), so a
|
|
1018
|
+
* `type: "detail"` page with the same `entitySource` shows one organisation
|
|
1019
|
+
* with the header, pills, facts and `@objectId` / `@object.*` tokens an object
|
|
1020
|
+
* page has.
|
|
1021
|
+
*
|
|
1022
|
+
* @return {object} The source adapter.
|
|
1023
|
+
* @spec openspec/changes/organisations-entity-source/specs/organisations-entity-source/spec.md
|
|
1024
|
+
*/
|
|
1025
|
+
function organisationsSource() {
|
|
1026
|
+
const store = useOrganisationStore()
|
|
1027
|
+
|
|
1028
|
+
const statusLabels = {
|
|
1029
|
+
provisioning: t('nextcloud-vue', 'Provisioning'),
|
|
1030
|
+
active: t('nextcloud-vue', 'Active'),
|
|
1031
|
+
suspended: t('nextcloud-vue', 'Suspended'),
|
|
1032
|
+
deprovisioning: t('nextcloud-vue', 'Deprovisioning'),
|
|
1033
|
+
archived: t('nextcloud-vue', 'Archived'),
|
|
1034
|
+
retained: t('nextcloud-vue', 'Retained'),
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return {
|
|
1038
|
+
store,
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Load the viewer's organisations. The endpoint takes no parameters.
|
|
1042
|
+
*
|
|
1043
|
+
* @return {Promise<void>} Resolves when loaded.
|
|
1044
|
+
*/
|
|
1045
|
+
load: () => store.load(),
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* @return {Array<object>} The rows, keyed on the organisation uuid.
|
|
1049
|
+
*/
|
|
1050
|
+
rows: () => (store.organisations || []).map(organisationRecord),
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* @return {boolean} Whether a load is in flight.
|
|
1054
|
+
*/
|
|
1055
|
+
loading: () => !!store.loading,
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Read one organisation for a detail page.
|
|
1059
|
+
*
|
|
1060
|
+
* @param {string} id The organisation uuid.
|
|
1061
|
+
* @return {Promise<object|null>} The organisation, or null when it could not be read.
|
|
1062
|
+
*/
|
|
1063
|
+
loadRecord: (id) => store.fetchRecord(id),
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* @param {string} id The organisation uuid.
|
|
1067
|
+
* @return {object|null} The organisation read for this id, keyed on its uuid, or null.
|
|
1068
|
+
*/
|
|
1069
|
+
record: (id) => {
|
|
1070
|
+
const organisation = store.records?.[String(id)]
|
|
1071
|
+
return organisation ? organisationRecord(organisation) : null
|
|
1072
|
+
},
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* @param {string} id The organisation uuid.
|
|
1076
|
+
* @return {number|null} The HTTP status of a failed read of this id, or null.
|
|
1077
|
+
*/
|
|
1078
|
+
recordError: (id) => store.recordErrors?.[String(id)] ?? null,
|
|
1079
|
+
|
|
1080
|
+
columns: [
|
|
1081
|
+
{ key: 'name', label: t('nextcloud-vue', 'Name') },
|
|
1082
|
+
{
|
|
1083
|
+
key: 'status',
|
|
1084
|
+
label: t('nextcloud-vue', 'Status'),
|
|
1085
|
+
enum: Object.keys(statusLabels),
|
|
1086
|
+
enumLabels: statusLabels,
|
|
1087
|
+
colorMap: { active: 'success', provisioning: 'info', suspended: 'warning', deprovisioning: 'error', archived: 'default', retained: 'default' },
|
|
1088
|
+
},
|
|
1089
|
+
{ key: 'type', label: t('nextcloud-vue', 'Type') },
|
|
1090
|
+
{ key: 'created', label: t('nextcloud-vue', 'Created'), type: 'string', format: 'date-time' },
|
|
1091
|
+
],
|
|
1092
|
+
|
|
1093
|
+
// Creating an organisation is not the index's object form, and
|
|
1094
|
+
// OpenRegister has no delete for one, so neither action is offered.
|
|
1095
|
+
showAdd: false,
|
|
1096
|
+
title: t('nextcloud-vue', 'Organisations'),
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
717
1100
|
/**
|
|
718
1101
|
* The registered sources, by the name a manifest uses.
|
|
719
1102
|
*
|
|
@@ -726,6 +1109,9 @@ export const indexSources = {
|
|
|
726
1109
|
flowRuns: flowRunsSource,
|
|
727
1110
|
tasks: tasksSource,
|
|
728
1111
|
endpoint: endpointSource,
|
|
1112
|
+
search: searchSource,
|
|
1113
|
+
users: usersSource,
|
|
1114
|
+
organisations: organisationsSource,
|
|
729
1115
|
}
|
|
730
1116
|
|
|
731
1117
|
/**
|
|
@@ -738,10 +1124,11 @@ export const indexSources = {
|
|
|
738
1124
|
* indistinguishable from a source that genuinely has no rows.
|
|
739
1125
|
*
|
|
740
1126
|
* @param {string} name The source name from `config.source`.
|
|
1127
|
+
* @param {object} [config] The page's source config, for a source that builds its tabs from it.
|
|
741
1128
|
*
|
|
742
1129
|
* @return {object|null} The adapter, or null when the name is not registered.
|
|
743
1130
|
*/
|
|
744
|
-
export function resolveIndexSource(name) {
|
|
1131
|
+
export function resolveIndexSource(name, config) {
|
|
745
1132
|
const key = String(name || '')
|
|
746
1133
|
if (!key) {
|
|
747
1134
|
return null
|
|
@@ -754,5 +1141,33 @@ export function resolveIndexSource(name) {
|
|
|
754
1141
|
return null
|
|
755
1142
|
}
|
|
756
1143
|
|
|
757
|
-
return factory()
|
|
1144
|
+
return factory(config || {})
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* Resolve a named source for ONE record, for a `type: "detail"` page.
|
|
1149
|
+
*
|
|
1150
|
+
* The same registry as the index: a source that can also read a single record
|
|
1151
|
+
* supplies `loadRecord(id)` and `record(id)`, and the detail page names it with
|
|
1152
|
+
* the same `entitySource`. A name that is unknown, or a source that cannot read
|
|
1153
|
+
* one record, resolves to null with a console warning, and the page shows no
|
|
1154
|
+
* record rather than the wrong one.
|
|
1155
|
+
*
|
|
1156
|
+
* @param {string} name The source name from `config.entitySource`.
|
|
1157
|
+
* @return {object|null} The adapter, or null.
|
|
1158
|
+
* @spec openspec/changes/organisations-entity-source/specs/organisations-entity-source/spec.md
|
|
1159
|
+
*/
|
|
1160
|
+
export function resolveRecordSource(name) {
|
|
1161
|
+
const key = String(name || '')
|
|
1162
|
+
if (!key) {
|
|
1163
|
+
return null
|
|
1164
|
+
}
|
|
1165
|
+
const factory = indexSources[key]
|
|
1166
|
+
const source = typeof factory === 'function' ? factory() : null
|
|
1167
|
+
if (!source || typeof source.loadRecord !== 'function' || typeof source.record !== 'function') {
|
|
1168
|
+
// eslint-disable-next-line no-console
|
|
1169
|
+
console.warn(`[CnDetailPage] entitySource "${key}" is not a source that reads one record. The page shows no record.`)
|
|
1170
|
+
return null
|
|
1171
|
+
}
|
|
1172
|
+
return source
|
|
758
1173
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
3
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import axios from '@nextcloud/axios'
|
|
7
|
+
import { generateUrl } from '@nextcloud/router'
|
|
8
|
+
import { defineStore } from 'pinia'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* OpenRegister's organisation endpoints, behind the `organisations` entity source.
|
|
12
|
+
*
|
|
13
|
+
* An organisation is not a register/schema object, so the object store cannot
|
|
14
|
+
* read it. Both reads answer from the session: the list holds only the
|
|
15
|
+
* organisations the viewer belongs to, and one organisation answers 403 when
|
|
16
|
+
* the viewer has no access to it. This store adds no check of its own and
|
|
17
|
+
* skips none.
|
|
18
|
+
*
|
|
19
|
+
* @type {string}
|
|
20
|
+
*/
|
|
21
|
+
export const ORGANISATIONS_URL = '/apps/openregister/api/organisations'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One organisation as a row or a record: the organisation as OpenRegister
|
|
25
|
+
* serialises it, with `id` set to its uuid. An OpenRegister object's `id` is
|
|
26
|
+
* its uuid as well, so `@object.id`, a row route and the page's `objectId`
|
|
27
|
+
* all name the same thing. The numeric database id stays out of sight.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} organisation The organisation from the endpoint.
|
|
30
|
+
* @return {object} The organisation keyed on its uuid.
|
|
31
|
+
*/
|
|
32
|
+
export function organisationRecord(organisation) {
|
|
33
|
+
return { ...organisation, id: organisation?.uuid ?? organisation?.id ?? null }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The organisations store.
|
|
38
|
+
*
|
|
39
|
+
* @spec openspec/changes/organisations-entity-source/specs/organisations-entity-source/spec.md
|
|
40
|
+
*/
|
|
41
|
+
export const useOrganisationStore = defineStore('cnOrganisations', {
|
|
42
|
+
state: () => ({
|
|
43
|
+
/** @type {Array<object>} The viewer's organisations, as the list endpoint returned them. */
|
|
44
|
+
organisations: [],
|
|
45
|
+
/** @type {boolean} Whether the list load is in flight. */
|
|
46
|
+
loading: false,
|
|
47
|
+
/** @type {string|null} The last list failure, for the console trail. */
|
|
48
|
+
error: null,
|
|
49
|
+
/** @type {Record<string, object>} Single organisations read so far, by uuid. */
|
|
50
|
+
records: {},
|
|
51
|
+
/** @type {Record<string, number|null>} The HTTP status of a failed single read, by uuid. */
|
|
52
|
+
recordErrors: {},
|
|
53
|
+
}),
|
|
54
|
+
|
|
55
|
+
actions: {
|
|
56
|
+
/**
|
|
57
|
+
* Load the viewer's organisations. The endpoint takes no parameters and
|
|
58
|
+
* returns every organisation the viewer belongs to in one response.
|
|
59
|
+
*
|
|
60
|
+
* @return {Promise<void>} Resolves when the rows are in the store.
|
|
61
|
+
*/
|
|
62
|
+
async load() {
|
|
63
|
+
this.loading = true
|
|
64
|
+
this.error = null
|
|
65
|
+
try {
|
|
66
|
+
const response = await axios.get(generateUrl(ORGANISATIONS_URL))
|
|
67
|
+
this.organisations = Array.isArray(response.data?.results) ? response.data.results : []
|
|
68
|
+
} catch (error) {
|
|
69
|
+
this.error = error?.message || String(error)
|
|
70
|
+
this.organisations = []
|
|
71
|
+
// eslint-disable-next-line no-console
|
|
72
|
+
console.error('[useOrganisationStore] Loading the organisations failed', error)
|
|
73
|
+
} finally {
|
|
74
|
+
this.loading = false
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read one organisation into `records`. A refusal (403) or a missing
|
|
80
|
+
* organisation (404) leaves no record and keeps the status in
|
|
81
|
+
* `recordErrors`, so the page can tell "not found" from "still loading".
|
|
82
|
+
*
|
|
83
|
+
* @param {string} uuid The organisation uuid.
|
|
84
|
+
* @return {Promise<object|null>} The organisation, or null when it could not be read.
|
|
85
|
+
*/
|
|
86
|
+
async fetchRecord(uuid) {
|
|
87
|
+
const key = String(uuid || '')
|
|
88
|
+
if (key === '') {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const response = await axios.get(generateUrl(`${ORGANISATIONS_URL}/${encodeURIComponent(key)}`))
|
|
93
|
+
const organisation = response.data?.organisation || null
|
|
94
|
+
const { [key]: dropped, ...errors } = this.recordErrors
|
|
95
|
+
this.recordErrors = errors
|
|
96
|
+
if (!organisation) {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
this.records = { ...this.records, [key]: organisation }
|
|
100
|
+
return organisation
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const { [key]: dropped, ...records } = this.records
|
|
103
|
+
this.records = records
|
|
104
|
+
this.recordErrors = { ...this.recordErrors, [key]: error?.response?.status ?? null }
|
|
105
|
+
// eslint-disable-next-line no-console
|
|
106
|
+
console.error(`[useOrganisationStore] Loading organisation "${key}" failed`, error)
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
})
|
package/src/index.js
CHANGED
|
@@ -59,6 +59,7 @@ export {
|
|
|
59
59
|
CnChartWidget,
|
|
60
60
|
CnChartWidgetForm,
|
|
61
61
|
CnChatPage,
|
|
62
|
+
CnChecklistRunWidget,
|
|
62
63
|
CnChildRecordsField,
|
|
63
64
|
CnChipsFieldWidget,
|
|
64
65
|
CnChoiceCards,
|
|
@@ -236,6 +237,7 @@ export {
|
|
|
236
237
|
CnObjectRow,
|
|
237
238
|
CnObjectSidebar,
|
|
238
239
|
CnOfflineQueue,
|
|
240
|
+
CnOrderedListField,
|
|
239
241
|
CnPageHeader,
|
|
240
242
|
CnPageRenderer,
|
|
241
243
|
CnPagination,
|