@conduction/nextcloud-vue 2.100.0 → 2.101.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/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +22 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +26 -16
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +51 -7
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +104 -73
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js +9 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js +139 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js +76 -0
- package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +160 -2
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +206 -94
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesReview.js +61 -0
- package/dist/esm/components/CnFilesBrowser/filesReview.js.map +1 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +9 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +279 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +122 -0
- package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnGroupedCardsWidget/index.js +29 -0
- package/dist/esm/components/CnGroupedCardsWidget/index.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +251 -71
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +317 -209
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js +4 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +15 -0
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +14 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +14 -6
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +10 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +6 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +91 -4
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +63 -38
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +9 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js +175 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js +77 -0
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +35 -2
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +4 -0
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.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 +167 -20
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useEndpointSource.js +28 -6
- package/dist/esm/composables/useEndpointSource.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 +37 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +35 -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 +769 -251
- package/dist/esm/utils/languageMapText.js +50 -0
- package/dist/esm/utils/languageMapText.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +3 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/mapList.js +71 -0
- package/dist/esm/utils/mapList.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/nextcloud-vue.cjs.js +8977 -6843
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +769 -251
- package/l10n/en.json +37 -1
- package/l10n/nl.json +35 -1
- package/package.json +1 -1
- package/src/components/CnChoiceCards/CnChoiceCards.md +39 -0
- package/src/components/CnChoiceCards/CnChoiceCards.vue +93 -16
- package/src/components/CnDashboardPage/CnDashboardPage.vue +89 -20
- package/src/components/CnExportsWidget/CnExportsWidget.vue +233 -0
- package/src/components/CnExportsWidget/index.js +7 -0
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +324 -0
- package/src/components/CnFilesBrowser/filesReview.js +57 -0
- package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +433 -0
- package/src/components/CnGroupedCardsWidget/index.js +28 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +240 -4
- package/src/components/CnIndexPage/useNamedSource.js +4 -1
- package/src/components/CnMapWidget/CnMapWidget.vue +37 -0
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +29 -1
- package/src/components/CnObjectSidebar/CnFilesTab.vue +15 -0
- package/src/components/CnOrderedListField/CnOrderedListField.vue +151 -5
- package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +272 -0
- package/src/components/CnSavedViewAlertWidget/index.js +7 -0
- package/src/components/CnSetupWizard/CnSetupWizard.vue +35 -2
- package/src/components/CnWidgetGrid/builtInWidgets.js +4 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/index.js +3 -0
- package/src/composables/indexSources.js +166 -19
- package/src/composables/useEndpointSource.js +28 -6
- package/src/css/index-page.css +117 -0
- package/src/css/look-board.css +44 -0
- package/src/index.js +1 -0
- package/src/schemas/app-manifest-v2.schema.json +31 -1
- package/src/utils/languageMapText.js +46 -0
- package/src/utils/libraryWidgetKeys.js +3 -0
- package/src/utils/mapList.js +67 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -11,6 +11,8 @@ import HandBackRight from 'vue-material-design-icons/HandBackRight.vue'
|
|
|
11
11
|
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
|
+
import { fetchReferenceObjects } from '../utils/boardReferenceColumns.js'
|
|
15
|
+
import { fillTemplate } from '../utils/fillTemplate.js'
|
|
14
16
|
import { FLOW_RUN_ERROR_STATUSES, flowRunStatuses } from '../utils/flowRunStatus.js'
|
|
15
17
|
import { fetchGroupMembers, fetchPresence, fetchStatuses } from '../utils/nextcloudDirectory.js'
|
|
16
18
|
import { readPath } from '../utils/readPath.js'
|
|
@@ -648,6 +650,10 @@ function readRowsPath(body, path) {
|
|
|
648
650
|
* requested: page-config keys must never become query parameters. The list
|
|
649
651
|
* stays empty and one console error names the page.
|
|
650
652
|
*
|
|
653
|
+
* A quick filter with `showCount` is counted with `count(config)`: the same
|
|
654
|
+
* request with that tab's filter, its total read at `sourceConfig.totalPath`
|
|
655
|
+
* (`total` by default), else the number of rows the answer holds.
|
|
656
|
+
*
|
|
651
657
|
* @return {object} The source adapter.
|
|
652
658
|
* @spec openspec/changes/index-endpoint-source/specs/index-page/spec.md#requirement-an-index-page-can-list-rows-from-an-app-endpoint
|
|
653
659
|
*/
|
|
@@ -656,6 +662,36 @@ function endpointSource() {
|
|
|
656
662
|
let seq = 0
|
|
657
663
|
let warned = false
|
|
658
664
|
|
|
665
|
+
/**
|
|
666
|
+
* The query parameters for a config: every key but the url and the paths,
|
|
667
|
+
* empty values left out, a list joined by commas.
|
|
668
|
+
*
|
|
669
|
+
* @param {object} rest The config without `url`, `rowsPath` and `totalPath`.
|
|
670
|
+
* @return {object} The parameters.
|
|
671
|
+
*/
|
|
672
|
+
const paramsOf = (rest) => {
|
|
673
|
+
const params = {}
|
|
674
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
675
|
+
if (value === undefined || value === null || value === '') {
|
|
676
|
+
continue
|
|
677
|
+
}
|
|
678
|
+
params[key] = Array.isArray(value) ? value.join(',') : (typeof value === 'object' ? JSON.stringify(value) : value)
|
|
679
|
+
}
|
|
680
|
+
return params
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Whether a config can be requested: it names a url and is not the page
|
|
685
|
+
* config handed over in its place.
|
|
686
|
+
*
|
|
687
|
+
* @param {object} config The config.
|
|
688
|
+
* @return {boolean}
|
|
689
|
+
*/
|
|
690
|
+
const requestable = (config) => {
|
|
691
|
+
const url = config && config.url
|
|
692
|
+
return typeof url === 'string' && url !== '' && !Object.hasOwn(config, 'entitySource')
|
|
693
|
+
}
|
|
694
|
+
|
|
659
695
|
return {
|
|
660
696
|
/**
|
|
661
697
|
* Fetch the rows.
|
|
@@ -664,10 +700,9 @@ function endpointSource() {
|
|
|
664
700
|
* @return {Promise<void>} Resolves when loaded; a failed request leaves no rows and rethrows.
|
|
665
701
|
*/
|
|
666
702
|
load: async (config) => {
|
|
667
|
-
const { url, rowsPath, ...rest } = config || {}
|
|
703
|
+
const { url, rowsPath, totalPath, ...rest } = config || {}
|
|
668
704
|
const mine = ++seq
|
|
669
|
-
|
|
670
|
-
if (typeof url !== 'string' || url === '' || pageConfig) {
|
|
705
|
+
if (!requestable(config)) {
|
|
671
706
|
state.rows = []
|
|
672
707
|
if (!warned) {
|
|
673
708
|
warned = true
|
|
@@ -677,13 +712,7 @@ function endpointSource() {
|
|
|
677
712
|
}
|
|
678
713
|
return
|
|
679
714
|
}
|
|
680
|
-
const params =
|
|
681
|
-
for (const [key, value] of Object.entries(rest)) {
|
|
682
|
-
if (value === undefined || value === null || value === '') {
|
|
683
|
-
continue
|
|
684
|
-
}
|
|
685
|
-
params[key] = Array.isArray(value) ? value.join(',') : (typeof value === 'object' ? JSON.stringify(value) : value)
|
|
686
|
-
}
|
|
715
|
+
const params = paramsOf(rest)
|
|
687
716
|
state.loading = true
|
|
688
717
|
try {
|
|
689
718
|
const { default: axios } = await import('@nextcloud/axios')
|
|
@@ -705,6 +734,29 @@ function endpointSource() {
|
|
|
705
734
|
}
|
|
706
735
|
},
|
|
707
736
|
|
|
737
|
+
/**
|
|
738
|
+
* Count the rows for a config without touching the rows on screen, for
|
|
739
|
+
* a quick-filter tab with `showCount`.
|
|
740
|
+
*
|
|
741
|
+
* @param {object} config The page's source config with the tab's filter merged in.
|
|
742
|
+
* @return {Promise<number>} The total, or NaN when nothing can be requested.
|
|
743
|
+
*/
|
|
744
|
+
count: async (config) => {
|
|
745
|
+
if (!requestable(config)) {
|
|
746
|
+
return Number.NaN
|
|
747
|
+
}
|
|
748
|
+
const { url, rowsPath, totalPath, ...rest } = config
|
|
749
|
+
const { default: axios } = await import('@nextcloud/axios')
|
|
750
|
+
const res = await axios.get(generateUrl(url), { params: paramsOf(rest) })
|
|
751
|
+
const body = res ? res.data : null
|
|
752
|
+
const total = body && typeof body === 'object' && !Array.isArray(body) ? readRowsPath(body, totalPath || 'total') : undefined
|
|
753
|
+
if (typeof total === 'number' && Number.isFinite(total)) {
|
|
754
|
+
return total
|
|
755
|
+
}
|
|
756
|
+
const rows = Array.isArray(body) ? body : readRowsPath(body, rowsPath || 'results')
|
|
757
|
+
return Array.isArray(rows) ? rows.length : Number.NaN
|
|
758
|
+
},
|
|
759
|
+
|
|
708
760
|
/**
|
|
709
761
|
* @return {Array<object>} The rows the endpoint returned.
|
|
710
762
|
*/
|
|
@@ -737,39 +789,102 @@ function fillRowTemplate(template, row) {
|
|
|
737
789
|
* and its count (DqZoekresultaten).
|
|
738
790
|
*
|
|
739
791
|
* `sourceConfig.kinds` lists what is searched:
|
|
740
|
-
* `{ id, label, register, schema, titleField?, subtitleField?, statusField?, href?, limit? }`
|
|
792
|
+
* `{ id, label, register, schema, titleField?, subtitleField?, statusField?, statusResolve?, linkedField?, linkedTemplate?, href?, limit? }`
|
|
741
793
|
* for objects (OpenRegister's `_search`, so the reader sees what they may
|
|
742
794
|
* read), or `{ id, label, files: true, limit? }` for the reader's files
|
|
743
795
|
* (Nextcloud's unified search). The term is `sourceConfig.term`, else the
|
|
744
796
|
* address's `?q=` (or `sourceConfig.termQuery`). A kind that fails shows no
|
|
745
797
|
* rows and no count; the others still answer.
|
|
746
798
|
*
|
|
799
|
+
* The kind's `label` goes through the app's translate (the "Soort" cell and
|
|
800
|
+
* the quick filters). `statusResolve: { register?, schema, labelField? }`
|
|
801
|
+
* reads `statusField` as a reference and shows the referenced object's label
|
|
802
|
+
* (`labelField`, else `title`, then `name`); the status column is a pill,
|
|
803
|
+
* coloured by `sourceConfig.statusColorMap`. `linkedTemplate` (a field
|
|
804
|
+
* template, "{case.title}") or `linkedField` (a path) fills the "Linked"
|
|
805
|
+
* column, drawn when a kind declares one; `sourceConfig.linkedLabel` names it.
|
|
806
|
+
* The page's title and count line can name the term as `{term}`, and the
|
|
807
|
+
* count is the active kind's total. Kind, link and status are filterable.
|
|
808
|
+
*
|
|
747
809
|
* @param {object} [config] The page's source config.
|
|
810
|
+
* @param {object} [context] The page's helpers.
|
|
811
|
+
* @param {Function} [context.translate] The app's translate.
|
|
748
812
|
* @return {object} The source adapter.
|
|
749
813
|
* @spec openspec/changes/index-search-source/specs/index-page/spec.md#requirement-an-index-page-can-search-several-schemas-and-files
|
|
750
814
|
*/
|
|
751
|
-
function searchSource(config = {}) {
|
|
815
|
+
function searchSource(config = {}, context = {}) {
|
|
816
|
+
const translate = typeof context.translate === 'function' ? context.translate : (text) => text
|
|
752
817
|
const kinds = (Array.isArray(config && config.kinds) ? config.kinds : [])
|
|
753
818
|
.filter((kind) => kind && typeof kind.id === 'string' && kind.id !== '' && (kind.files === true || (kind.register && kind.schema)))
|
|
754
|
-
|
|
819
|
+
.map((kind) => ({ ...kind, label: translate(String(kind.label || kind.id)) }))
|
|
820
|
+
const hasLinked = kinds.some((kind) => kind.linkedTemplate || kind.linkedField)
|
|
821
|
+
const state = reactive({ rows: [], totals: {}, loading: false, kind: 'all', term: '', filters: {} })
|
|
755
822
|
let seq = 0
|
|
756
823
|
|
|
824
|
+
/**
|
|
825
|
+
* The labels of the objects a kind's status points at, by id: the
|
|
826
|
+
* referenced schema read once per search.
|
|
827
|
+
*
|
|
828
|
+
* @param {object} kind The kind with `statusResolve`.
|
|
829
|
+
* @return {Promise<Map<string, string>>} Labels by id; empty when it cannot be read.
|
|
830
|
+
*/
|
|
831
|
+
const statusLabels = async (kind) => {
|
|
832
|
+
const resolve = kind.statusResolve
|
|
833
|
+
if (!resolve || !resolve.schema) {
|
|
834
|
+
return new Map()
|
|
835
|
+
}
|
|
836
|
+
const objects = await fetchReferenceObjects({ register: resolve.register || kind.register, schema: resolve.schema }).catch(() => [])
|
|
837
|
+
const labels = new Map()
|
|
838
|
+
for (const object of objects) {
|
|
839
|
+
const id = object.id || object.uuid || (object['@self'] && object['@self'].id)
|
|
840
|
+
const label = readPath(object, resolve.labelField || 'title') ?? object.name
|
|
841
|
+
if (id !== undefined && label !== undefined && label !== null) {
|
|
842
|
+
labels.set(String(id), String(label))
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return labels
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* What a row shows in the Linked column.
|
|
850
|
+
*
|
|
851
|
+
* @param {object} kind The row's kind.
|
|
852
|
+
* @param {object} object The found object.
|
|
853
|
+
* @return {string}
|
|
854
|
+
*/
|
|
855
|
+
const linkedOf = (kind, object) => {
|
|
856
|
+
if (kind.linkedTemplate) {
|
|
857
|
+
return fillTemplate(translate(kind.linkedTemplate), object)
|
|
858
|
+
}
|
|
859
|
+
if (kind.linkedField) {
|
|
860
|
+
const value = readPath(object, kind.linkedField)
|
|
861
|
+
return value === undefined || value === null ? '' : String(value)
|
|
862
|
+
}
|
|
863
|
+
return ''
|
|
864
|
+
}
|
|
865
|
+
|
|
757
866
|
const searchObjects = async (axios, kind, term) => {
|
|
758
867
|
const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: kind.register, schema: kind.schema })
|
|
759
868
|
const res = await axios.get(url, { params: { _search: term, _limit: kind.limit || 20 } })
|
|
760
869
|
const body = (res && res.data) || {}
|
|
761
870
|
const results = Array.isArray(body.results) ? body.results : (Array.isArray(body) ? body : [])
|
|
871
|
+
const labels = kind.statusResolve && kind.statusField ? await statusLabels(kind) : null
|
|
762
872
|
return {
|
|
763
873
|
total: typeof body.total === 'number' ? body.total : results.length,
|
|
764
874
|
rows: results.map((object) => {
|
|
765
875
|
const id = object.id || object.uuid || (object['@self'] && object['@self'].id)
|
|
876
|
+
const rawStatus = kind.statusField ? readPath(object, kind.statusField) : undefined
|
|
877
|
+
const status = rawStatus === undefined || rawStatus === null
|
|
878
|
+
? ''
|
|
879
|
+
: (labels ? (labels.get(String(rawStatus)) || '') : String(rawStatus))
|
|
766
880
|
return {
|
|
767
881
|
id: `${kind.id}:${id}`,
|
|
768
882
|
kind: kind.id,
|
|
769
883
|
kindLabel: kind.label,
|
|
770
884
|
title: String(readPath(object, kind.titleField || 'title') ?? object.name ?? id ?? ''),
|
|
771
885
|
subtitle: kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : '',
|
|
772
|
-
status
|
|
886
|
+
status,
|
|
887
|
+
linked: linkedOf(kind, object),
|
|
773
888
|
href: kind.href ? generateUrl(fillRowTemplate(kind.href, { ...object, id })) : '',
|
|
774
889
|
object,
|
|
775
890
|
}
|
|
@@ -792,6 +907,7 @@ function searchSource(config = {}) {
|
|
|
792
907
|
title: entry.title || '',
|
|
793
908
|
subtitle: entry.subline || '',
|
|
794
909
|
status: '',
|
|
910
|
+
linked: '',
|
|
795
911
|
href: entry.resourceUrl || '',
|
|
796
912
|
})),
|
|
797
913
|
}
|
|
@@ -806,6 +922,17 @@ function searchSource(config = {}) {
|
|
|
806
922
|
*/
|
|
807
923
|
load: async (cfg) => {
|
|
808
924
|
state.kind = cfg && typeof cfg.kind === 'string' ? cfg.kind : 'all'
|
|
925
|
+
// The header filters on Kind, Linked and Status narrow the answer
|
|
926
|
+
// already fetched; they do not search again.
|
|
927
|
+
const filters = {}
|
|
928
|
+
for (const key of ['kindLabel', 'linked', 'status']) {
|
|
929
|
+
const value = cfg && cfg[key]
|
|
930
|
+
const values = (Array.isArray(value) ? value : (value === undefined || value === null || value === '' ? [] : [value])).map(String)
|
|
931
|
+
if (values.length > 0) {
|
|
932
|
+
filters[key] = values
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
state.filters = filters
|
|
809
936
|
const query = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : null
|
|
810
937
|
const term = String((cfg && cfg.term) || (query && query.get((cfg && cfg.termQuery) || 'q')) || '').trim()
|
|
811
938
|
const mine = ++seq
|
|
@@ -837,7 +964,20 @@ function searchSource(config = {}) {
|
|
|
837
964
|
/**
|
|
838
965
|
* @return {Array<object>} The results of the active kind, or all of them.
|
|
839
966
|
*/
|
|
840
|
-
rows: () => (state.kind === 'all' ? state.rows : state.rows.filter((row) => row.kind === state.kind))
|
|
967
|
+
rows: () => (state.kind === 'all' ? state.rows : state.rows.filter((row) => row.kind === state.kind))
|
|
968
|
+
.filter((row) => Object.entries(state.filters).every(([key, values]) => values.includes(String(row[key])))),
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* @return {string} The term searched for, for a `{term}` in the title or count line.
|
|
972
|
+
*/
|
|
973
|
+
term: () => state.term,
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* @return {number} The active kind's total, or every kind's together on All.
|
|
977
|
+
*/
|
|
978
|
+
total: () => (state.kind === 'all'
|
|
979
|
+
? Object.values(state.totals).reduce((sum, value) => sum + value, 0)
|
|
980
|
+
: (state.totals[state.kind] || 0)),
|
|
841
981
|
|
|
842
982
|
/**
|
|
843
983
|
* @return {boolean} Whether a search is in flight.
|
|
@@ -846,8 +986,14 @@ function searchSource(config = {}) {
|
|
|
846
986
|
|
|
847
987
|
columns: [
|
|
848
988
|
{ key: 'title', label: t('nextcloud-vue', 'Result'), secondary: 'subtitle' },
|
|
849
|
-
{ key: 'kindLabel', label: t('nextcloud-vue', 'Kind') },
|
|
850
|
-
{ key: '
|
|
989
|
+
{ key: 'kindLabel', label: t('nextcloud-vue', 'Kind'), enum: kinds.map((kind) => kind.label) },
|
|
990
|
+
...(hasLinked ? [{ key: 'linked', label: config.linkedLabel ? translate(config.linkedLabel) : t('nextcloud-vue', 'Linked') }] : []),
|
|
991
|
+
{
|
|
992
|
+
key: 'status',
|
|
993
|
+
label: t('nextcloud-vue', 'Status'),
|
|
994
|
+
widget: 'badge',
|
|
995
|
+
widgetProps: { colorMap: (config && config.statusColorMap) || {} },
|
|
996
|
+
},
|
|
851
997
|
],
|
|
852
998
|
|
|
853
999
|
quickFilters: kinds.length > 0
|
|
@@ -1125,10 +1271,11 @@ export const indexSources = {
|
|
|
1125
1271
|
*
|
|
1126
1272
|
* @param {string} name The source name from `config.source`.
|
|
1127
1273
|
* @param {object} [config] The page's source config, for a source that builds its tabs from it.
|
|
1274
|
+
* @param {object} [context] The page's helpers a source may use, such as `translate` (the app's catalogue).
|
|
1128
1275
|
*
|
|
1129
1276
|
* @return {object|null} The adapter, or null when the name is not registered.
|
|
1130
1277
|
*/
|
|
1131
|
-
export function resolveIndexSource(name, config) {
|
|
1278
|
+
export function resolveIndexSource(name, config, context) {
|
|
1132
1279
|
const key = String(name || '')
|
|
1133
1280
|
if (!key) {
|
|
1134
1281
|
return null
|
|
@@ -1141,7 +1288,7 @@ export function resolveIndexSource(name, config) {
|
|
|
1141
1288
|
return null
|
|
1142
1289
|
}
|
|
1143
1290
|
|
|
1144
|
-
return factory(config || {})
|
|
1291
|
+
return factory(config || {}, context || {})
|
|
1145
1292
|
}
|
|
1146
1293
|
|
|
1147
1294
|
/**
|
|
@@ -155,7 +155,7 @@ export function interpolateUrlTokens(str, ctx) {
|
|
|
155
155
|
*
|
|
156
156
|
* @param {{url: string, method?: string, params?: object}} config The endpointSource block.
|
|
157
157
|
* @param {{objectId?: (string|number), object?: object, workspace?: object, config?: object, range?: object}} [ctx] The token context.
|
|
158
|
-
* @return {{url: string, method: ('GET'|'POST'), params: object, blocked: boolean}} The resolved request.
|
|
158
|
+
* @return {{url: string, method: ('GET'|'POST'), params: object, blocked: boolean, responsePath: string}} The resolved request.
|
|
159
159
|
*/
|
|
160
160
|
export function resolveEndpointRequest(config, ctx) {
|
|
161
161
|
const cfg = config || {}
|
|
@@ -163,8 +163,30 @@ export function resolveEndpointRequest(config, ctx) {
|
|
|
163
163
|
const resolved = resolveFilterTokens(cfg.params || {}, ctx)
|
|
164
164
|
const params = dropOptionalUnresolved(resolved)
|
|
165
165
|
const method = String(cfg.method || 'GET').toUpperCase() === 'POST' ? 'POST' : 'GET'
|
|
166
|
-
const
|
|
167
|
-
|
|
166
|
+
const responsePath = resolveResponsePath(cfg.responsePath, ctx)
|
|
167
|
+
const blocked = hasUnresolvedTokens(params) || hasEmptyPathSegment(cfg.url || '', url) || responsePath === null
|
|
168
|
+
return { url, method, params, blocked, responsePath: responsePath || '' }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolve the tokens in a `responsePath` (`phaseDays.@workspace.measure`), so a
|
|
173
|
+
* page control can choose which part of one response a widget reads. Null when
|
|
174
|
+
* a token resolved to nothing and left an empty path segment: the read waits
|
|
175
|
+
* instead of plucking the wrong part.
|
|
176
|
+
*
|
|
177
|
+
* @param {string|undefined} path The configured path.
|
|
178
|
+
* @param {object} [ctx] The token context.
|
|
179
|
+
* @return {string|null} The resolved path, '' for none, null when blocked.
|
|
180
|
+
*/
|
|
181
|
+
function resolveResponsePath(path, ctx) {
|
|
182
|
+
if (typeof path !== 'string' || path === '') {
|
|
183
|
+
return ''
|
|
184
|
+
}
|
|
185
|
+
if (path.indexOf('@') === -1) {
|
|
186
|
+
return path
|
|
187
|
+
}
|
|
188
|
+
const resolved = interpolateUrlTokens(path, ctx)
|
|
189
|
+
return resolved.split('.').some((segment) => segment === '') ? null : resolved
|
|
168
190
|
}
|
|
169
191
|
|
|
170
192
|
/**
|
|
@@ -309,7 +331,7 @@ export async function fetchEndpointSource(config, ctx, opts) {
|
|
|
309
331
|
return null
|
|
310
332
|
}
|
|
311
333
|
const body = await fetchSharedResponse(request, opts)
|
|
312
|
-
const payload = getByPath(body,
|
|
334
|
+
const payload = getByPath(body, request.responsePath)
|
|
313
335
|
return payload === undefined ? null : payload
|
|
314
336
|
}
|
|
315
337
|
|
|
@@ -378,7 +400,7 @@ export function useEndpointSource(source, options) {
|
|
|
378
400
|
u: request.url,
|
|
379
401
|
m: request.method,
|
|
380
402
|
p: request.params,
|
|
381
|
-
r:
|
|
403
|
+
r: request.responsePath,
|
|
382
404
|
b: request.blocked,
|
|
383
405
|
})
|
|
384
406
|
})
|
|
@@ -418,7 +440,7 @@ export function useEndpointSource(source, options) {
|
|
|
418
440
|
if (seq !== fetchSeq) {
|
|
419
441
|
return
|
|
420
442
|
}
|
|
421
|
-
const payload = getByPath(body,
|
|
443
|
+
const payload = getByPath(body, request.responsePath)
|
|
422
444
|
data.value = payload === undefined ? null : payload
|
|
423
445
|
} catch (e) {
|
|
424
446
|
if (seq !== fetchSeq) {
|
package/src/css/index-page.css
CHANGED
|
@@ -73,6 +73,123 @@
|
|
|
73
73
|
min-height: 300px;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/* The map with its numbered list (`mapConfig.list`, DqZakenKaart): one framed
|
|
77
|
+
card, the map taking the larger share and the list beside it, stacking
|
|
78
|
+
under a narrow width. */
|
|
79
|
+
.cn-index-page__map-card {
|
|
80
|
+
display: flex;
|
|
81
|
+
flex: 1;
|
|
82
|
+
flex-wrap: wrap;
|
|
83
|
+
gap: 16px;
|
|
84
|
+
min-height: 0;
|
|
85
|
+
padding: 16px;
|
|
86
|
+
border: 1px solid var(--color-border);
|
|
87
|
+
border-radius: var(--cn-board-card-radius, 12px);
|
|
88
|
+
background: var(--color-main-background);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.cn-index-page__map-card .cn-index-page__map {
|
|
92
|
+
flex: 3 1 420px;
|
|
93
|
+
min-width: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.cn-index-page__map-side {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex: 2 1 320px;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
gap: 12px;
|
|
101
|
+
min-width: 0;
|
|
102
|
+
overflow-y: auto;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.cn-index-page__map-list {
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-direction: column;
|
|
108
|
+
margin: 0;
|
|
109
|
+
padding: 0;
|
|
110
|
+
border: 1px solid var(--color-border);
|
|
111
|
+
border-radius: 10px;
|
|
112
|
+
list-style: none;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.cn-index-page__map-item {
|
|
116
|
+
display: flex;
|
|
117
|
+
gap: 12px;
|
|
118
|
+
padding: 12px 14px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.cn-index-page__map-item + .cn-index-page__map-item {
|
|
122
|
+
border-top: 1px solid var(--color-border);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.cn-index-page__map-item--active {
|
|
126
|
+
background: var(--color-primary-element-light);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.cn-index-page__map-number {
|
|
130
|
+
display: inline-flex;
|
|
131
|
+
flex: none;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
|
+
width: 26px;
|
|
135
|
+
height: 26px;
|
|
136
|
+
border-radius: 13px;
|
|
137
|
+
background: var(--color-primary-element);
|
|
138
|
+
color: var(--color-primary-element-text);
|
|
139
|
+
font-size: 13px;
|
|
140
|
+
font-weight: 700;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.cn-index-page__map-item--active .cn-index-page__map-number {
|
|
144
|
+
background: var(--color-primary-element-light-text);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.cn-index-page__map-text {
|
|
148
|
+
display: flex;
|
|
149
|
+
flex: 1;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
gap: 4px;
|
|
152
|
+
min-width: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.cn-index-page__map-title {
|
|
156
|
+
margin: 0;
|
|
157
|
+
padding: 0;
|
|
158
|
+
border: 0;
|
|
159
|
+
background: none;
|
|
160
|
+
color: var(--color-main-text);
|
|
161
|
+
font: inherit;
|
|
162
|
+
font-size: 15px;
|
|
163
|
+
font-weight: 600;
|
|
164
|
+
line-height: 1.35;
|
|
165
|
+
text-align: start;
|
|
166
|
+
cursor: pointer;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.cn-index-page__map-sub,
|
|
170
|
+
.cn-index-page__map-due {
|
|
171
|
+
color: var(--color-text-maxcontrast);
|
|
172
|
+
font-size: 13px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.cn-index-page__map-pills {
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-wrap: wrap;
|
|
178
|
+
align-items: center;
|
|
179
|
+
gap: 6px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.cn-index-page__map-missing {
|
|
183
|
+
display: flex;
|
|
184
|
+
flex-direction: column;
|
|
185
|
+
align-items: flex-start;
|
|
186
|
+
gap: 8px;
|
|
187
|
+
padding: 14px;
|
|
188
|
+
border: 1px solid var(--color-border);
|
|
189
|
+
border-radius: 10px;
|
|
190
|
+
font-size: 14px;
|
|
191
|
+
}
|
|
192
|
+
|
|
76
193
|
/* Opt-in folder navigation pane (left of the list). */
|
|
77
194
|
.cn-index-page__folder-pane {
|
|
78
195
|
flex: 0 0 240px;
|
package/src/css/look-board.css
CHANGED
|
@@ -99,6 +99,50 @@
|
|
|
99
99
|
padding-inline-start: 56px;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/* The dashboard header as the board draws it (DqTermijnen): the actions on
|
|
103
|
+
the title's row, the description and its segmented period under both, at
|
|
104
|
+
the header's full width. As two flex columns the description sat in the
|
|
105
|
+
title's column only, so a long period group wrapped under its text and the
|
|
106
|
+
actions dropped to a line of their own. The title column never shrinks
|
|
107
|
+
below its longest word; the actions wrap their buttons first.
|
|
108
|
+
@spec openspec/changes/dashboard-board-header-row/specs/dashboard-page/spec.md#requirement-the-board-dashboard-header-keeps-its-actions-on-the-title-row */
|
|
109
|
+
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-dashboard-page__header {
|
|
110
|
+
display: grid;
|
|
111
|
+
grid-template-columns: minmax(min-content, 1fr) auto;
|
|
112
|
+
align-items: start;
|
|
113
|
+
column-gap: 16px;
|
|
114
|
+
row-gap: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-dashboard-page__header-left {
|
|
118
|
+
display: contents;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-dashboard-page__title-row {
|
|
122
|
+
grid-column: 1;
|
|
123
|
+
grid-row: 1;
|
|
124
|
+
align-self: center;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-dashboard-page__header-left > :not(.cn-dashboard-page__title-row) {
|
|
128
|
+
grid-column: 1 / -1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-dashboard-page__header > .cn-dashboard-page__header-actions {
|
|
132
|
+
grid-column: 2;
|
|
133
|
+
grid-row: 1;
|
|
134
|
+
justify-content: flex-end;
|
|
135
|
+
margin-inline-start: 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* A stat tile in the board's KPI row carries the board's own 18px 20px inset
|
|
139
|
+
(`cn-kpi-card--board`), so the card-fit cell adds none of its own: with
|
|
140
|
+
both the label sat 29px from the tile's top and 37px from its side, and
|
|
141
|
+
the row stood about 1.4 times the board's height. */
|
|
142
|
+
.cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-dashboard-page__kpi-cell .cn-dashboard-page__card-fit .cn-widget-wrapper__content:has(> .cn-kpi-card--board.cn-kpi-card--stacked) {
|
|
143
|
+
padding: 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
102
146
|
/* ---- The header button ------------------------------------------------- */
|
|
103
147
|
.cn-look-board .cn-index-page__header-buttons:not(.cn-look-nextcloud *),
|
|
104
148
|
.cn-look-board .cn-detail-page__header-actions:not(.cn-look-nextcloud *),
|
package/src/index.js
CHANGED
|
@@ -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.124.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"title": { "type": "string" },
|
|
99
99
|
"body": { "type": "string", "description": "info: explanatory copy." },
|
|
100
100
|
"note": { "type": "string", "minLength": 1, "description": "choice: a footnote under the options, translated like body." },
|
|
101
|
+
"visibleWhen": { "description": "Offer the step only when the setup status says so: a dotted path into the setup status document whose value must be truthy, or { path, equals } for an exact value. Hidden while the status loads.", "oneOf": [ { "type": "string", "minLength": 1 }, { "type": "object", "required": ["path"], "additionalProperties": false, "properties": { "path": { "type": "string", "minLength": 1 }, "equals": {} } } ] },
|
|
101
102
|
"footnote": { "type": "string", "minLength": 1, "description": "choice: the same footnote under another name; note wins when both are set." },
|
|
102
103
|
"required": { "type": "boolean", "default": false, "description": "When true the step gates the app until done." },
|
|
103
104
|
"schema": { "description": "config-fields: a JSON Schema ref (string) or inline object rendered via fieldsFromSchema." },
|
|
@@ -4372,6 +4373,35 @@
|
|
|
4372
4373
|
"default": true,
|
|
4373
4374
|
"description": "type='dashboard' and type='detail': `false` drops the overflow Actions menu from every widget that does not set `showActions` itself, for a dashboard whose widget headers carry a header link and nothing else (zuiddrecht-pixel-gaps-2), or a record page whose data cards carry none (zuiddrecht-pixel-gaps-3; a card with an Add action keeps it). When the key is not set a detail page keeps the menu, except under the board look, where side cards carry none (screens-detail-page-parity). Maps to CnDashboardPage / CnDetailPage `showWidgetActions`."
|
|
4374
4375
|
},
|
|
4376
|
+
"pageFilters": {
|
|
4377
|
+
"type": "array",
|
|
4378
|
+
"description": "type='dashboard': page-level filter controls in the header. Each selection is written into the page's workspace context, so a widget reads it through a `@page.<key>` / `@workspace.<key>` token (an endpoint widget also in its `responsePath`). Maps to the CnDashboardPage `pageFilters` prop.",
|
|
4379
|
+
"items": {
|
|
4380
|
+
"type": "object",
|
|
4381
|
+
"required": ["key", "options"],
|
|
4382
|
+
"additionalProperties": false,
|
|
4383
|
+
"properties": {
|
|
4384
|
+
"key": { "type": "string", "minLength": 1, "description": "Workspace-context key written on change (the `@page.<key>` / `@workspace.<key>` token name)." },
|
|
4385
|
+
"label": { "type": "string", "description": "Control label (i18n key or plain text)." },
|
|
4386
|
+
"type": { "type": "string", "enum": ["select"], "default": "select", "description": "Control type, carried over from v1." },
|
|
4387
|
+
"control": { "type": "string", "enum": ["select", "segmented"], "default": "select", "description": "How the filter is drawn: a select, or a compact segmented group." },
|
|
4388
|
+
"placement": { "type": "string", "enum": ["row", "description"], "default": "row", "description": "Where the filter sits: in the header's filter row, or (a segmented group) on the description line after the period group (DqProcesanalyse)." },
|
|
4389
|
+
"default": { "type": ["string", "number"], "description": "Initial value seeded into the context on mount; the first option's value when omitted." },
|
|
4390
|
+
"options": {
|
|
4391
|
+
"type": "array",
|
|
4392
|
+
"items": {
|
|
4393
|
+
"type": "object",
|
|
4394
|
+
"required": ["value", "label"],
|
|
4395
|
+
"additionalProperties": false,
|
|
4396
|
+
"properties": {
|
|
4397
|
+
"value": { "type": ["string", "number"], "description": "Value written into the context when picked." },
|
|
4398
|
+
"label": { "type": "string", "description": "Display text (i18n key or plain text)." }
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
},
|
|
4375
4405
|
"kpiRow": {
|
|
4376
4406
|
"type": "array",
|
|
4377
4407
|
"items": { "type": "string" },
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getLanguage } from '@nextcloud/l10n'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The user's Nextcloud language as a bare code (`nl_NL` reads `nl`), or ''.
|
|
10
|
+
*
|
|
11
|
+
* @return {string} The code.
|
|
12
|
+
*/
|
|
13
|
+
function userLanguage() {
|
|
14
|
+
try {
|
|
15
|
+
return String(getLanguage() || '').split(/[-_]/)[0].toLowerCase()
|
|
16
|
+
} catch {
|
|
17
|
+
return ''
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The text of a value that may be a language map (`{ nl: 'Ontvangst', en:
|
|
23
|
+
* 'Intake' }`), in the user's Nextcloud language, then Dutch, then English,
|
|
24
|
+
* then the first text it holds. A plain value comes back as text; anything
|
|
25
|
+
* else as ''.
|
|
26
|
+
*
|
|
27
|
+
* @param {(string|number|object|null|undefined)} value The value.
|
|
28
|
+
* @return {string} The text.
|
|
29
|
+
* @spec openspec/changes/grouped-cards-widget/specs/grouped-cards-widget/spec.md#requirement-a-widget-draws-groups-of-cards-from-an-endpoint
|
|
30
|
+
*/
|
|
31
|
+
export function languageMapText(value) {
|
|
32
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
33
|
+
return String(value)
|
|
34
|
+
}
|
|
35
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
36
|
+
return ''
|
|
37
|
+
}
|
|
38
|
+
const language = userLanguage()
|
|
39
|
+
for (const key of [language, 'nl', 'en']) {
|
|
40
|
+
if (key && typeof value[key] === 'string' && value[key] !== '') {
|
|
41
|
+
return value[key]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const first = Object.values(value).find((text) => typeof text === 'string' && text !== '')
|
|
45
|
+
return first || ''
|
|
46
|
+
}
|
|
@@ -71,6 +71,8 @@ export const BUILT_IN_WIDGET_KEYS = Object.freeze([
|
|
|
71
71
|
'header',
|
|
72
72
|
'text',
|
|
73
73
|
'divider',
|
|
74
|
+
'saved-view-alert',
|
|
75
|
+
'exports',
|
|
74
76
|
])
|
|
75
77
|
|
|
76
78
|
/**
|
|
@@ -109,6 +111,7 @@ export const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
|
|
|
109
111
|
'files',
|
|
110
112
|
'flow-runs',
|
|
111
113
|
'gauge',
|
|
114
|
+
'grouped-cards',
|
|
112
115
|
'header',
|
|
113
116
|
'image',
|
|
114
117
|
'interaction-form',
|