@conduction/nextcloud-vue 2.99.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/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +6 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +2 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- 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/CnDetailPage/CnDetailPage.vue2.js +25 -8
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +5 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -8
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +13 -4
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.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/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +54 -3
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +16 -2
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- 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/CnFlowsPage/CnFlowBoard.vue.js +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js +42 -7
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js +121 -57
- package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js +10 -0
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js +2 -1
- package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js.map +1 -1
- 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 +387 -78
- 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/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +68 -7
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +256 -240
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.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/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js +1 -1
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js +38 -9
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +51 -4
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +40 -25
- 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/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +7 -7
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.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 +43 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +41 -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 +1101 -396
- 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 +9996 -7378
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1101 -396
- package/l10n/en.json +43 -1
- package/l10n/nl.json +41 -1
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +7 -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/CnDetailPage/CnDetailPage.vue +28 -9
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +13 -4
- package/src/components/CnExportsWidget/CnExportsWidget.vue +233 -0
- package/src/components/CnExportsWidget/index.js +7 -0
- package/src/components/CnFactsWidget/CnFactsWidget.vue +65 -3
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +324 -0
- package/src/components/CnFilesBrowser/filesReview.js +57 -0
- package/src/components/CnFlowsPage/CnFlowBoard.vue +77 -2
- package/src/components/CnFlowsPage/CnFlowEditorPage.vue +11 -0
- package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +433 -0
- package/src/components/CnGroupedCardsWidget/index.js +28 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +376 -11
- 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/CnRecordListWidget/CnRecordListWidget.vue +114 -7
- package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +272 -0
- package/src/components/CnSavedViewAlertWidget/index.js +7 -0
- package/src/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue +38 -9
- package/src/components/CnSetupWizard/CnSetupWizard.vue +72 -5
- package/src/components/CnWidgetGrid/builtInWidgets.js +4 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/CnWizardDialog/CnWizardDialog.vue +7 -7
- package/src/components/index.js +3 -0
- package/src/composables/indexSources.js +166 -19
- package/src/composables/useEndpointSource.js +28 -6
- package/src/css/detail-page.css +15 -0
- package/src/css/dialog.css +48 -0
- package/src/css/index-page.css +117 -0
- package/src/css/look-board.css +127 -1
- package/src/index.js +1 -0
- package/src/schemas/app-manifest-v2.schema.json +55 -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/detail-page.css
CHANGED
|
@@ -525,3 +525,18 @@
|
|
|
525
525
|
align-items: center;
|
|
526
526
|
gap: 4px;
|
|
527
527
|
}
|
|
528
|
+
|
|
529
|
+
/* The tag after an admin-only entry in a board detail page's Actions menu
|
|
530
|
+
("beheerder", DqZaakMeer): 12px muted text at the end of the row. The menu
|
|
531
|
+
opens outside the page, so the rule keys on the attribute CnDetailPage sets
|
|
532
|
+
only in the board look. */
|
|
533
|
+
.action-button[data-cn-admin-tag]::after,
|
|
534
|
+
.action-link[data-cn-admin-tag]::after {
|
|
535
|
+
content: attr(data-cn-admin-tag);
|
|
536
|
+
margin-inline-start: auto;
|
|
537
|
+
padding-inline: 12px 4px;
|
|
538
|
+
color: var(--color-text-maxcontrast);
|
|
539
|
+
font-size: 12px;
|
|
540
|
+
font-weight: 400;
|
|
541
|
+
white-space: nowrap;
|
|
542
|
+
}
|
package/src/css/dialog.css
CHANGED
|
@@ -173,6 +173,54 @@
|
|
|
173
173
|
color: var(--color-main-text);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/* A Nextcloud text field or text area in a board dialog is the board's field:
|
|
177
|
+
its label above the control at 14px/600, the control 40px with a 1px grey
|
|
178
|
+
border and radius 8 (PlanItemDialog, DqWerkvoorraad). NcInputField draws
|
|
179
|
+
its border as a 65%-transparent shadow, which on the white dialog read as
|
|
180
|
+
an empty band, and floats its label inside the border. A field with
|
|
181
|
+
`labelOutside` keeps the app's own label and takes the same control. */
|
|
182
|
+
.cn-dialog-board .dialog__content .input-field:has(.input-field__label),
|
|
183
|
+
.cn-dialog-board .dialog__content .textarea:has(.textarea__label) {
|
|
184
|
+
margin-block-start: 26px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* The doubled class outranks NcInputField's raised-label rule (focused or
|
|
188
|
+
filled), which would pin the label's top as well and squash it. */
|
|
189
|
+
.cn-dialog-board .dialog__content .input-field__input + .input-field__label.input-field__label,
|
|
190
|
+
.cn-dialog-board .dialog__content .textarea__input + .textarea__label.textarea__label {
|
|
191
|
+
inset-block-start: auto;
|
|
192
|
+
inset-block-end: calc(100% + 4px);
|
|
193
|
+
inset-inline: 0 auto;
|
|
194
|
+
max-width: 100%;
|
|
195
|
+
margin: 0;
|
|
196
|
+
padding: 0;
|
|
197
|
+
background: none;
|
|
198
|
+
color: var(--color-main-text);
|
|
199
|
+
font-size: var(--cn-field-label-size, 14px);
|
|
200
|
+
font-weight: 600;
|
|
201
|
+
line-height: 20px;
|
|
202
|
+
pointer-events: auto;
|
|
203
|
+
transition: none;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.cn-dialog-board .dialog__content .input-field__main-wrapper {
|
|
207
|
+
height: calc(var(--cn-field-height, 40px) + 2 * var(--border-width-input-focused, 2px));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.cn-dialog-board .dialog__content .input-field__input,
|
|
211
|
+
.cn-dialog-board .dialog__content .input-field__input:hover:not([disabled]),
|
|
212
|
+
.cn-dialog-board .dialog__content .textarea__input,
|
|
213
|
+
.cn-dialog-board .dialog__content .textarea__input:hover:not([disabled]) {
|
|
214
|
+
border-radius: 8px;
|
|
215
|
+
box-shadow: 0 0 0 1px var(--color-border-dark);
|
|
216
|
+
font-size: var(--cn-field-text-size, 15px);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.cn-dialog-board .dialog__content .input-field:has(.input-field__label) .input-field__input:not(:focus)::placeholder,
|
|
220
|
+
.cn-dialog-board .dialog__content .textarea__input:not(:focus)::placeholder {
|
|
221
|
+
opacity: 1 !important;
|
|
222
|
+
}
|
|
223
|
+
|
|
176
224
|
/* Type-to-confirm: a labelled 40px field with a monospace value and a hint. */
|
|
177
225
|
.cn-dialog__confirm-field {
|
|
178
226
|
display: flex;
|
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;
|