@conduction/nextcloud-vue 2.96.0 → 2.97.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/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/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/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/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 +301 -18
- 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/composables/indexSources.js +182 -3
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/l10n/en.json.js +7 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +7 -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 +301 -177
- 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 +3009 -1907
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +301 -177
- package/l10n/en.json +7 -1
- package/l10n/nl.json +7 -1
- package/package.json +1 -1
- package/src/components/CnCellRenderer/CnCellRenderer.vue +57 -1
- 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/CnFactsWidget/CnFactsWidget.vue +114 -3
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +508 -236
- package/src/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
- package/src/components/CnIndexPage/CnIndexPage.vue +324 -24
- 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/composables/indexSources.js +182 -3
- package/src/schemas/app-manifest-v2.schema.json +11 -4
- package/src/utils/queryPresets.js +13 -2
- package/src/utils/scopeListLayout.js +37 -1
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { translate as t } from '@nextcloud/l10n'
|
|
7
|
-
import { generateUrl } from '@nextcloud/router'
|
|
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,6 +12,7 @@ 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 { readPath } from '../utils/readPath.js'
|
|
15
16
|
import { useFlowStore } from './useFlowStore.js'
|
|
16
17
|
import { useTaskInboxStore } from './useTaskInboxStore.js'
|
|
17
18
|
|
|
@@ -714,6 +715,182 @@ function endpointSource() {
|
|
|
714
715
|
}
|
|
715
716
|
}
|
|
716
717
|
|
|
718
|
+
/**
|
|
719
|
+
* Fill `{field}` placeholders in a link template from a row.
|
|
720
|
+
*
|
|
721
|
+
* @param {string} template A path such as `/apps/dossiq/cases/{id}`.
|
|
722
|
+
* @param {object} row The row.
|
|
723
|
+
* @return {string} The path.
|
|
724
|
+
*/
|
|
725
|
+
function fillRowTemplate(template, row) {
|
|
726
|
+
return String(template).replace(/\{([\w.@]+)\}/g, (match, path) => {
|
|
727
|
+
const value = readPath(row, path)
|
|
728
|
+
return value === undefined || value === null ? '' : encodeURIComponent(String(value))
|
|
729
|
+
})
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* The search source: one search term over several OpenRegister schemas and
|
|
734
|
+
* the reader's files (`entitySource: "search"`), with a quick filter per kind
|
|
735
|
+
* and its count (DqZoekresultaten).
|
|
736
|
+
*
|
|
737
|
+
* `sourceConfig.kinds` lists what is searched:
|
|
738
|
+
* `{ id, label, register, schema, titleField?, subtitleField?, statusField?, href?, limit? }`
|
|
739
|
+
* for objects (OpenRegister's `_search`, so the reader sees what they may
|
|
740
|
+
* read), or `{ id, label, files: true, limit? }` for the reader's files
|
|
741
|
+
* (Nextcloud's unified search). The term is `sourceConfig.term`, else the
|
|
742
|
+
* address's `?q=` (or `sourceConfig.termQuery`). A kind that fails shows no
|
|
743
|
+
* rows and no count; the others still answer.
|
|
744
|
+
*
|
|
745
|
+
* @param {object} [config] The page's source config.
|
|
746
|
+
* @return {object} The source adapter.
|
|
747
|
+
* @spec openspec/changes/index-search-source/specs/index-page/spec.md#requirement-an-index-page-can-search-several-schemas-and-files
|
|
748
|
+
*/
|
|
749
|
+
function searchSource(config = {}) {
|
|
750
|
+
const kinds = (Array.isArray(config && config.kinds) ? config.kinds : [])
|
|
751
|
+
.filter((kind) => kind && typeof kind.id === 'string' && kind.id !== '' && (kind.files === true || (kind.register && kind.schema)))
|
|
752
|
+
const state = reactive({ rows: [], totals: {}, loading: false, kind: 'all', term: '' })
|
|
753
|
+
let seq = 0
|
|
754
|
+
|
|
755
|
+
const searchObjects = async (axios, kind, term) => {
|
|
756
|
+
const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: kind.register, schema: kind.schema })
|
|
757
|
+
const res = await axios.get(url, { params: { _search: term, _limit: kind.limit || 20 } })
|
|
758
|
+
const body = (res && res.data) || {}
|
|
759
|
+
const results = Array.isArray(body.results) ? body.results : (Array.isArray(body) ? body : [])
|
|
760
|
+
return {
|
|
761
|
+
total: typeof body.total === 'number' ? body.total : results.length,
|
|
762
|
+
rows: results.map((object) => {
|
|
763
|
+
const id = object.id || object.uuid || (object['@self'] && object['@self'].id)
|
|
764
|
+
return {
|
|
765
|
+
id: `${kind.id}:${id}`,
|
|
766
|
+
kind: kind.id,
|
|
767
|
+
kindLabel: kind.label,
|
|
768
|
+
title: String(readPath(object, kind.titleField || 'title') ?? object.name ?? id ?? ''),
|
|
769
|
+
subtitle: kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : '',
|
|
770
|
+
status: kind.statusField ? String(readPath(object, kind.statusField) ?? '') : '',
|
|
771
|
+
href: kind.href ? generateUrl(fillRowTemplate(kind.href, { ...object, id })) : '',
|
|
772
|
+
object,
|
|
773
|
+
}
|
|
774
|
+
}),
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const searchFiles = async (axios, kind, term) => {
|
|
779
|
+
const res = await axios.get(generateOcsUrl('search/providers/files/search'), {
|
|
780
|
+
params: { term, limit: kind.limit || 20, format: 'json' },
|
|
781
|
+
headers: { 'OCS-APIRequest': 'true' },
|
|
782
|
+
})
|
|
783
|
+
const entries = (res && res.data && res.data.ocs && res.data.ocs.data && res.data.ocs.data.entries) || []
|
|
784
|
+
return {
|
|
785
|
+
total: entries.length,
|
|
786
|
+
rows: entries.map((entry, index) => ({
|
|
787
|
+
id: `${kind.id}:${entry.resourceUrl || index}`,
|
|
788
|
+
kind: kind.id,
|
|
789
|
+
kindLabel: kind.label,
|
|
790
|
+
title: entry.title || '',
|
|
791
|
+
subtitle: entry.subline || '',
|
|
792
|
+
status: '',
|
|
793
|
+
href: entry.resourceUrl || '',
|
|
794
|
+
})),
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return {
|
|
799
|
+
/**
|
|
800
|
+
* Run the search over every kind, in parallel.
|
|
801
|
+
*
|
|
802
|
+
* @param {object} cfg The page's source config with the active tab's `kind` merged in.
|
|
803
|
+
* @return {Promise<void>} Resolves when every kind answered or failed.
|
|
804
|
+
*/
|
|
805
|
+
load: async (cfg) => {
|
|
806
|
+
state.kind = cfg && typeof cfg.kind === 'string' ? cfg.kind : 'all'
|
|
807
|
+
const query = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : null
|
|
808
|
+
const term = String((cfg && cfg.term) || (query && query.get((cfg && cfg.termQuery) || 'q')) || '').trim()
|
|
809
|
+
const mine = ++seq
|
|
810
|
+
state.term = term
|
|
811
|
+
if (term === '' || kinds.length === 0) {
|
|
812
|
+
state.rows = []
|
|
813
|
+
state.totals = {}
|
|
814
|
+
return
|
|
815
|
+
}
|
|
816
|
+
state.loading = true
|
|
817
|
+
const { default: axios } = await import('@nextcloud/axios')
|
|
818
|
+
const answers = await Promise.all(kinds.map((kind) => (kind.files === true ? searchFiles(axios, kind, term) : searchObjects(axios, kind, term)).catch(() => null)))
|
|
819
|
+
if (mine !== seq) {
|
|
820
|
+
return
|
|
821
|
+
}
|
|
822
|
+
const totals = {}
|
|
823
|
+
const rows = []
|
|
824
|
+
answers.forEach((answer, index) => {
|
|
825
|
+
if (answer) {
|
|
826
|
+
totals[kinds[index].id] = answer.total
|
|
827
|
+
rows.push(...answer.rows)
|
|
828
|
+
}
|
|
829
|
+
})
|
|
830
|
+
state.totals = totals
|
|
831
|
+
state.rows = rows
|
|
832
|
+
state.loading = false
|
|
833
|
+
},
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* @return {Array<object>} The results of the active kind, or all of them.
|
|
837
|
+
*/
|
|
838
|
+
rows: () => (state.kind === 'all' ? state.rows : state.rows.filter((row) => row.kind === state.kind)),
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* @return {boolean} Whether a search is in flight.
|
|
842
|
+
*/
|
|
843
|
+
loading: () => state.loading,
|
|
844
|
+
|
|
845
|
+
columns: [
|
|
846
|
+
{ key: 'title', label: t('nextcloud-vue', 'Result'), secondary: 'subtitle' },
|
|
847
|
+
{ key: 'kindLabel', label: t('nextcloud-vue', 'Kind') },
|
|
848
|
+
{ key: 'status', label: t('nextcloud-vue', 'Status') },
|
|
849
|
+
],
|
|
850
|
+
|
|
851
|
+
quickFilters: kinds.length > 0
|
|
852
|
+
? [
|
|
853
|
+
{ label: t('nextcloud-vue', 'All'), filter: { kind: 'all' }, default: true },
|
|
854
|
+
...kinds.map((kind) => ({ label: kind.label || kind.id, filter: { kind: kind.id } })),
|
|
855
|
+
]
|
|
856
|
+
: null,
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* The number on each kind's quick filter: the total each search
|
|
860
|
+
* reported, and their sum on All.
|
|
861
|
+
*
|
|
862
|
+
* @return {Record<number, number>} The counts by tab index.
|
|
863
|
+
*/
|
|
864
|
+
tabCounts: () => {
|
|
865
|
+
const counts = { 0: Object.values(state.totals).reduce((sum, value) => sum + value, 0) }
|
|
866
|
+
kinds.forEach((kind, index) => {
|
|
867
|
+
if (typeof state.totals[kind.id] === 'number') {
|
|
868
|
+
counts[index + 1] = state.totals[kind.id]
|
|
869
|
+
}
|
|
870
|
+
})
|
|
871
|
+
return counts
|
|
872
|
+
},
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Open a result where it lives.
|
|
876
|
+
*
|
|
877
|
+
* @param {object} row The clicked row.
|
|
878
|
+
* @return {void}
|
|
879
|
+
*/
|
|
880
|
+
openRow: (row) => {
|
|
881
|
+
if (row && row.href) {
|
|
882
|
+
window.location.assign(row.href)
|
|
883
|
+
}
|
|
884
|
+
},
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* @param {object} row A row.
|
|
888
|
+
* @return {string|null} The URL the row opens, for a new tab.
|
|
889
|
+
*/
|
|
890
|
+
rowTarget: (row) => (row && row.href) || null,
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
717
894
|
/**
|
|
718
895
|
* The registered sources, by the name a manifest uses.
|
|
719
896
|
*
|
|
@@ -726,6 +903,7 @@ export const indexSources = {
|
|
|
726
903
|
flowRuns: flowRunsSource,
|
|
727
904
|
tasks: tasksSource,
|
|
728
905
|
endpoint: endpointSource,
|
|
906
|
+
search: searchSource,
|
|
729
907
|
}
|
|
730
908
|
|
|
731
909
|
/**
|
|
@@ -738,10 +916,11 @@ export const indexSources = {
|
|
|
738
916
|
* indistinguishable from a source that genuinely has no rows.
|
|
739
917
|
*
|
|
740
918
|
* @param {string} name The source name from `config.source`.
|
|
919
|
+
* @param {object} [config] The page's source config, for a source that builds its tabs from it.
|
|
741
920
|
*
|
|
742
921
|
* @return {object|null} The adapter, or null when the name is not registered.
|
|
743
922
|
*/
|
|
744
|
-
export function resolveIndexSource(name) {
|
|
923
|
+
export function resolveIndexSource(name, config) {
|
|
745
924
|
const key = String(name || '')
|
|
746
925
|
if (!key) {
|
|
747
926
|
return null
|
|
@@ -754,5 +933,5 @@ export function resolveIndexSource(name) {
|
|
|
754
933
|
return null
|
|
755
934
|
}
|
|
756
935
|
|
|
757
|
-
return factory()
|
|
936
|
+
return factory(config || {})
|
|
758
937
|
}
|
|
@@ -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.121.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -597,7 +597,7 @@
|
|
|
597
597
|
"folderSidebarScope": {
|
|
598
598
|
"type": "object",
|
|
599
599
|
"additionalProperties": true,
|
|
600
|
-
"description": "One folder in `config.folderSidebar.folders[]`, which doubles as a SCOPE: the three optional list-layout keys below let one index page show each folder the way that folder needs. Every other key (id, name, icon, ...) stays free-form, so an existing folder list validates unchanged. The page's `config.columns` still decides which columns this page HAS; a scope only decides which of them are shown, in what order, sorted by what and searched over what. A scope naming a column the page does not declare is refused by validateManifestV2, not silently rendered.",
|
|
600
|
+
"description": "One folder in `config.folderSidebar.folders[]`, which doubles as a SCOPE: the three optional list-layout keys below let one index page show each folder the way that folder needs. Every other key (id, name, icon, ...) stays free-form, so an existing folder list validates unchanged. A scope (or the `x-index` block of the row a register-derived folder comes from) may also carry the presentation keys a query preset sets, under the same names: title, quickFilters, cardFields, countText, searchPlaceholder, footerNote, viewSwitch, addLabel, headerButtons, breadcrumb, headerMeta and afterList (index-scope-presentation); a scope's value wins over a preset's. The page's `config.columns` still decides which columns this page HAS; a scope only decides which of them are shown, in what order, sorted by what and searched over what. A scope naming a column the page does not declare is refused by validateManifestV2, not silently rendered.",
|
|
601
601
|
"properties": {
|
|
602
602
|
"columns": {
|
|
603
603
|
"type": "array",
|
|
@@ -3702,9 +3702,10 @@
|
|
|
3702
3702
|
},
|
|
3703
3703
|
"entitySource": {
|
|
3704
3704
|
"type": "string",
|
|
3705
|
-
"enum": ["flows", "tasks"],
|
|
3705
|
+
"enum": ["flows", "flowRuns", "tasks", "endpoint", "search", "users"],
|
|
3706
3706
|
"description": "Name a registered NON-OBJECT entity collection for a type='index' page, instead of register+schema. Deliberately NOT called `source`: that key is already taken on page config and is polymorphic (a URL string in some manifests, an object with `params` in others), so reusing it would give one key two meanings and break six deployed shillinq pages. A flow definition is deliberately not an OpenRegister object, so an index had nothing to bind to and such lists became bespoke type='custom' pages. With a source the index loads the list itself. Takes precedence over register/schema; non-empty `objects` still wins over both. Registered sources live in src/composables/indexSources.js. `tasks` lists the viewer's task inbox from OpenRegister's flow-tasks read (cn-tasks-entity-source)."
|
|
3707
3707
|
},
|
|
3708
|
+
"sourceConfig": { "type": "object", "additionalProperties": true, "description": "type='index' with `entitySource`: what the named source loads with. `endpoint`: `url` (an app path, required) and `rowsPath` (default `results`); its other keys go along as query parameters. `search`: `kinds` (`{ id, label, register, schema, titleField?, subtitleField?, statusField?, href?, limit? }` or `{ id, label, files: true }`), `term` and `termQuery` (default `q`). `users`: `defaultGroup`, `functionField`, `rolesField`. `flows` / `tasks`: `app`, `scope`. Maps to CnIndexPage `sourceConfig`.", "properties": { "url": { "type": "string", "minLength": 1 }, "rowsPath": { "type": "string", "minLength": 1 }, "kinds": { "type": "array", "items": { "type": "object", "required": ["id"], "additionalProperties": true, "properties": { "id": { "type": "string", "minLength": 1 }, "label": { "type": "string" }, "register": { "type": "string" }, "schema": { "type": "string" }, "files": { "type": "boolean" } } } }, "term": { "type": "string" }, "termQuery": { "type": "string", "minLength": 1 }, "defaultGroup": { "type": "string", "minLength": 1 } } },
|
|
3708
3709
|
"app": {
|
|
3709
3710
|
"type": "string",
|
|
3710
3711
|
"description": "Scopes a named `entitySource` to one app (e.g. entitySource='flows' with app='dossiq' lists that app's flows)."
|
|
@@ -4427,7 +4428,12 @@
|
|
|
4427
4428
|
"searchPlaceholder": { "type": "string", "minLength": 1, "description": "The search placeholder while the preset applies." },
|
|
4428
4429
|
"footerNote": { "type": "string", "minLength": 1, "description": "The footer note while the preset applies." },
|
|
4429
4430
|
"defaultSort": { "type": "array", "description": "The default sort while the preset applies; same shape as config.defaultSort." },
|
|
4430
|
-
"viewSwitch": { "type": "array", "items": { "type": "string", "enum": ["table", "cards", "board", "map"] }, "description": "The drawn view segments while the preset applies." }
|
|
4431
|
+
"viewSwitch": { "type": "array", "items": { "type": "string", "enum": ["table", "cards", "board", "map"] }, "description": "The drawn view segments while the preset applies." },
|
|
4432
|
+
"addLabel": { "type": "string", "minLength": 1, "description": "The add button label while the preset applies (index-scope-presentation)." },
|
|
4433
|
+
"headerButtons": { "type": "array", "description": "The header buttons while the preset applies; same shape as config.headerButtons (index-scope-presentation)." },
|
|
4434
|
+
"breadcrumb": { "description": "The breadcrumb while the preset applies; same shape as config.breadcrumb (index-scope-presentation).", "anyOf": [{ "$ref": "#/$defs/breadcrumbCrumb" }, { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/breadcrumbCrumb" } }] },
|
|
4435
|
+
"headerMeta": { "type": "string", "minLength": 1, "description": "The note after the breadcrumb while the preset applies (index-scope-presentation)." },
|
|
4436
|
+
"afterList": { "type": "array", "description": "The cards after the list while the preset applies; same shape as config.afterList (index-scope-presentation).", "items": { "type": "object", "required": ["id", "type"], "properties": { "id": { "type": "string", "minLength": 1 }, "type": { "type": "string", "minLength": 1 }, "title": { "type": "string" }, "subtitle": { "type": "string" }, "content": { "type": "object" }, "headerLink": { "type": "object" } } } }
|
|
4431
4437
|
}
|
|
4432
4438
|
}
|
|
4433
4439
|
},
|
|
@@ -4478,6 +4484,7 @@
|
|
|
4478
4484
|
"description": "type='index': the property keys the cards view shows as the facts list of each card, in order; defaults to the first four list columns under the board look (screens-card-parity). Maps to CnIndexPage `cardFields`.",
|
|
4479
4485
|
"items": { "type": "string", "minLength": 1 }
|
|
4480
4486
|
},
|
|
4487
|
+
"afterList": { "type": "array", "description": "type='index': cards after the list, stacked full width in the listed order, the counterpart of statRow above it (index-scope-presentation). Each entry is a widget definition resolved like statRow's. A query preset or a folder scope can set its own. Maps to CnIndexPage `afterList`.", "items": { "type": "object", "required": ["id", "type"], "properties": { "id": { "type": "string", "minLength": 1 }, "type": { "type": "string", "minLength": 1 }, "title": { "type": "string" }, "subtitle": { "type": "string" }, "content": { "type": "object" }, "headerLink": { "type": "object" } } } },
|
|
4481
4488
|
"statRow": {
|
|
4482
4489
|
"type": "array",
|
|
4483
4490
|
"default": [],
|
|
@@ -30,6 +30,11 @@ export const QUERY_PRESET_KEYS = Object.freeze([
|
|
|
30
30
|
'footerNote',
|
|
31
31
|
'defaultSort',
|
|
32
32
|
'viewSwitch',
|
|
33
|
+
'addLabel',
|
|
34
|
+
'headerButtons',
|
|
35
|
+
'breadcrumb',
|
|
36
|
+
'headerMeta',
|
|
37
|
+
'afterList',
|
|
33
38
|
])
|
|
34
39
|
|
|
35
40
|
/**
|
|
@@ -71,8 +76,9 @@ export function matchQueryPreset(presets, query) {
|
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
/**
|
|
74
|
-
* The page config with the preset's allowed keys laid over it,
|
|
75
|
-
* `queryPresets` itself (the page component
|
|
79
|
+
* The page config with the preset's allowed keys laid over it, its `match`
|
|
80
|
+
* as `fixedFilters`, and without `queryPresets` itself (the page component
|
|
81
|
+
* has no such prop).
|
|
76
82
|
*
|
|
77
83
|
* @param {object} config The page config.
|
|
78
84
|
* @param {object|null} preset The matched preset, or null.
|
|
@@ -89,5 +95,10 @@ export function applyQueryPreset(config, preset) {
|
|
|
89
95
|
out[key] = preset[key]
|
|
90
96
|
}
|
|
91
97
|
}
|
|
98
|
+
// The query pairs that chose the preset scope the list: the page shows
|
|
99
|
+
// them as no removable filter chip.
|
|
100
|
+
if (preset.match && typeof preset.match === 'object') {
|
|
101
|
+
out.fixedFilters = { ...preset.match }
|
|
102
|
+
}
|
|
92
103
|
return out
|
|
93
104
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { QUERY_PRESET_KEYS } from './queryPresets.js'
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* The three list-layout keys a `folderSidebar` scope may carry.
|
|
8
10
|
*
|
|
@@ -10,6 +12,15 @@
|
|
|
10
12
|
*/
|
|
11
13
|
const SCOPE_LAYOUT_KEYS = ['columns', 'defaultSort', 'searchFields']
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The presentation keys a scope may carry beside its list layout: the keys a
|
|
17
|
+
* query preset may set (`QUERY_PRESET_KEYS`), less the two the list layout
|
|
18
|
+
* resolves itself (`columns`, `defaultSort`).
|
|
19
|
+
*
|
|
20
|
+
* @type {ReadonlyArray<string>}
|
|
21
|
+
*/
|
|
22
|
+
const SCOPE_PRESENTATION_KEYS = Object.freeze(QUERY_PRESET_KEYS.filter((key) => key !== 'columns' && key !== 'defaultSort'))
|
|
23
|
+
|
|
13
24
|
/**
|
|
14
25
|
* The key of a column entry, in either of the two shapes the manifest accepts:
|
|
15
26
|
* a bare string (legacy shorthand) or a `{ key, label, ... }` object.
|
|
@@ -149,4 +160,29 @@ export function resolveScopeLayout({ scope = null, row = null, pageColumns = []
|
|
|
149
160
|
return { columns, sortKeys, searchFields }
|
|
150
161
|
}
|
|
151
162
|
|
|
152
|
-
|
|
163
|
+
/**
|
|
164
|
+
* The presentation a scope asks for: each of `SCOPE_PRESENTATION_KEYS` the
|
|
165
|
+
* folder entry declares, else the one its schema row's `x-index` declares.
|
|
166
|
+
* Only declared keys are in the result, so the page's own value stands for
|
|
167
|
+
* every other key. These are the keys a query preset sets, under the same
|
|
168
|
+
* names, and a scope's value wins over a preset's because it is applied on
|
|
169
|
+
* top of the page config the preset produced.
|
|
170
|
+
*
|
|
171
|
+
* @param {object} options Resolution inputs.
|
|
172
|
+
* @param {(object|null)} [options.scope] The folder entry for the active scope.
|
|
173
|
+
* @param {(object|null)} [options.row] The schema row the folder derives from.
|
|
174
|
+
* @return {object} The declared presentation keys.
|
|
175
|
+
* @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-a-preset-or-a-scope-sets-the-list-s-presentation
|
|
176
|
+
*/
|
|
177
|
+
export function resolveScopePresentation({ scope = null, row = null } = {}) {
|
|
178
|
+
const out = {}
|
|
179
|
+
for (const key of SCOPE_PRESENTATION_KEYS) {
|
|
180
|
+
const value = declaredOrRow(scope, row, key)
|
|
181
|
+
if (value !== undefined) {
|
|
182
|
+
out[key] = value
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export { SCOPE_LAYOUT_KEYS, SCOPE_PRESENTATION_KEYS }
|