@conduction/nextcloud-vue 2.111.0 → 2.113.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/CnActionButtons/CnActionButtons.vue.js +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +4 -6
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +3 -2
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +4 -2
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +3 -2
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +2 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +54 -5
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +16 -2
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +10 -0
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +2 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +22 -2
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +17 -3
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- 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 +18 -3
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +52 -13
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +31 -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 +48 -11
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +160 -97
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +2 -2
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +222 -15
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +24 -7
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +1 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +21 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +36 -2
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +44 -5
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -0
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js.map +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +22 -2
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js +36 -16
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +29 -12
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +3 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -1
- package/dist/esm/composables/useRefLabels.js +26 -6
- package/dist/esm/composables/useRefLabels.js.map +1 -1
- package/dist/esm/integrations/registry.js +4 -0
- package/dist/esm/integrations/registry.js.map +1 -1
- package/dist/esm/l10n/en.json.js +3 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +3 -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 +680 -449
- package/dist/esm/store/plugins/liveUpdates.js +3 -1
- package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
- package/dist/esm/store/plugins/logs.js +2 -1
- package/dist/esm/store/plugins/logs.js.map +1 -1
- package/dist/esm/utils/actionLink.js +104 -1
- package/dist/esm/utils/actionLink.js.map +1 -1
- package/dist/esm/utils/detailActionModel.js +28 -1
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +13 -5
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/headerFieldChips.js +21 -2
- package/dist/esm/utils/headerFieldChips.js.map +1 -1
- package/dist/esm/utils/mapList.js +26 -11
- package/dist/esm/utils/mapList.js.map +1 -1
- package/dist/esm/utils/savedViewMenu.js +12 -5
- package/dist/esm/utils/savedViewMenu.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 +2800 -1933
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +680 -449
- package/l10n/en.json +3 -1
- package/l10n/nl.json +3 -1
- package/package.json +1 -1
- package/src/components/CnActionButtons/CnActionButtons.vue +4 -6
- package/src/components/CnActionDialog/CnActionDialog.vue +32 -9
- package/src/components/CnAssignDialog/CnAssignDialog.vue +4 -2
- package/src/components/CnBoardView/CnBoardView.vue +58 -5
- package/src/components/CnCardGrid/CnCardGrid.md +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +11 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +27 -3
- package/src/components/CnChartWidget/CnChartWidget.vue +6 -0
- package/src/components/CnChoiceCards/CnChoiceCards.vue +0 -3
- package/src/components/CnDashboardPage/CnDashboardPage.vue +20 -5
- package/src/components/CnDetailPage/CnDetailPage.vue +62 -14
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +100 -5
- package/src/components/CnFormDialog/CnFormDialog.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +233 -18
- package/src/components/CnNcDashboardList/CnNcDashboardList.vue +21 -1
- package/src/components/CnObjectCard/CnObjectCard.md +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +51 -5
- package/src/components/CnPartyPicker/CnPartyPicker.vue +62 -16
- package/src/components/CnReportsPage/CnReportsPage.vue +31 -12
- package/src/components/CnSearchSelect/CnSearchSelect.vue +8 -0
- package/src/composables/useRefLabels.js +25 -5
- package/src/css/badge.css +15 -13
- package/src/css/board.css +0 -1
- package/src/css/dialog.css +53 -3
- package/src/css/index-page.css +26 -1
- package/src/css/look-board-detail.css +36 -15
- package/src/css/look-board-index.css +67 -15
- package/src/css/look-board.css +32 -18
- package/src/integrations/registry.js +4 -0
- package/src/schemas/app-manifest-v2.schema.json +61 -13
- package/src/store/plugins/liveUpdates.js +3 -1
- package/src/store/plugins/logs.js +2 -1
- package/src/utils/actionLink.js +103 -0
- package/src/utils/detailActionModel.js +27 -0
- package/src/utils/fillTemplate.js +13 -5
- package/src/utils/headerFieldChips.js +21 -2
- package/src/utils/mapList.js +26 -11
- package/src/utils/savedViewMenu.js +12 -5
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnPartyPicker.vue3.js","sources":["../../../../src/components/CnPartyPicker/CnPartyPicker.vue?vue&type=template&id=6ac01095&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<CnSearchSelect\n\t\tclass=\"cn-party-picker\"\n\t\t:modelValue=\"selectedOption\"\n\t\t:options=\"options\"\n\t\t:inputLabel=\"inputLabel\"\n\t\t:labelOutside=\"labelOutside\"\n\t\t:placeholder=\"placeholder ? cnTranslate(placeholder) : ''\"\n\t\t:loading=\"searching\"\n\t\t:filterable=\"false\"\n\t\t:clearable=\"clearable\"\n\t\t:disabled=\"disabled\"\n\t\tdata-testid=\"cn-party-picker\"\n\t\t@search=\"onSearch\"\n\t\t@update:modelValue=\"onPick\" />\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { generateUrl } from '@nextcloud/router'\nimport { fillTemplate } from '../../utils/fillTemplate.js'\nimport { CnSearchSelect } from '../CnSearchSelect/index.js'\n\n/** How long typing rests before a search runs, in ms. */\nconst SEARCH_DELAY = 250\n\n/**\n * CnPartyPicker — one search field over several kinds of party: people and\n * organisations from OpenRegister register sets (BRP persons, KvK companies,\n * an app's own clients) and the reader's Nextcloud contacts, as rows with\n * initials, the name and a second line, in one list (board DqNieuweZaak\n * \"Aanvrager\": \"Henk Visser · Inwoner · Beukenlaan 3\", \"Visser Bouw B.V. ·\n * Organisatie · KvK …\"). Built on CnSearchSelect.\n *\n * `sources`: each `{ id, register, schema, label?, title?, secondary?,\n * limit?, icon? }` searches OpenRegister with `_search`; `title` and\n * `secondary` are templates filled from the row (default `{name}` / none),\n * `label` names the kind (\"Resident\") and starts the second line. A source\n * `{ id, type: \"contacts\", label? }` searches the reader's Nextcloud contacts.\n *\n * `update:modelValue` emits the chosen row's id (a contact has none and\n * emits its contact id); `select` emits the whole choice: `{ source, id,\n * label, secondary, row }`, so a host can store more than the id (a case's\n * initiator type, source id and display name). A `modelValue` the list does\n * not hold is shown by its `selectedLabel`, else looked up by id in the\n * OpenRegister sources.\n *\n * ```vue\n * <CnPartyPicker\n * v-model=\"case.requester\"\n * input-label=\"Requester\"\n * :sources=\"[\n * { id: 'person', register: 'dossiq', schema: 'brpPerson', label: 'Resident', title: '{firstName} {lastName}', secondary: '{residence.street} {residence.houseNumber}' },\n * { id: 'company', register: 'dossiq', schema: 'kvkCompany', label: 'Organisation', title: '{tradeName}', secondary: 'KvK {sourceId}' },\n * { id: 'contact', type: 'contacts', label: 'Contact' },\n * ]\"\n * @select=\"onRequester\" />\n * ```\n */\nexport default {\n\tname: 'CnPartyPicker',\n\n\tcomponents: { CnSearchSelect },\n\n\tinject: {\n\t\t/** The host app's translate (CnAppRoot `cnTranslate`). */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** The chosen party's id. */\n\t\tmodelValue: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Where to search: OpenRegister sets `{ id, register, schema, label?,\n\t\t * title?, secondary?, limit?, icon? }`, or `{ id, type: \"contacts\",\n\t\t * label? }` for the reader's Nextcloud contacts.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tsources: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The field's label. */\n\t\tinputLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Draw the label above the field. */\n\t\tlabelOutside: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Placeholder (\"Name, BSN, trade name or KvK number\"), through the host translate. */\n\t\tplaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The chosen party's name, shown before the list holds it. */\n\t\tselectedLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Offer a clear button. */\n\t\tclearable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Disable the field. */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** The fewest characters a search needs. */\n\t\tminLength: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\t},\n\n\temits: ['update:modelValue', 'select'],\n\n\tdata() {\n\t\treturn {\n\t\t\toptions: [],\n\t\t\tsearching: false,\n\t\t\tknown: null,\n\t\t\ttimer: null,\n\t\t\tsearchSeq: 0,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The option for `modelValue`: in the list, else the known choice,\n\t\t * else one named by `selectedLabel`.\n\t\t *\n\t\t * @spec openspec/changes/party-picker/specs/party-picker/spec.md#requirement-one-search-field-over-several-kinds-of-party\n\t\t * @return {object|null}\n\t\t */\n\t\tselectedOption() {\n\t\t\tconst value = this.modelValue\n\t\t\tif (value === null || value === undefined || value === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst found = this.options.find((o) => String(o.id) === String(value))\n\t\t\tif (found) {\n\t\t\t\treturn found\n\t\t\t}\n\t\t\tif (this.known && String(this.known.id) === String(value)) {\n\t\t\t\treturn this.known\n\t\t\t}\n\t\t\treturn { id: value, label: this.selectedLabel || String(value), avatar: true }\n\t\t},\n\t},\n\n\twatch: {\n\t\tmodelValue: {\n\t\t\timmediate: true,\n\t\t\thandler(value) {\n\t\t\t\tif (value && !this.selectedLabel && !this.options.some((o) => String(o.id) === String(value))) {\n\t\t\t\t\tthis.lookUp(value)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tclearTimeout(this.timer)\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A source's row as a picker option.\n\t\t *\n\t\t * @param {object} source The source.\n\t\t * @param {object} row The row.\n\t\t * @return {object|null} The option, or null without an id.\n\t\t */\n\t\toptionOf(source, row) {\n\t\t\tconst id = row && (row.id ?? row['@self']?.id ?? row.uuid)\n\t\t\tif (id === undefined || id === null || id === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst title = (source.title ? fillTemplate(source.title, row) : (row.name || row.title || row.displayName || '')).trim() || String(id)\n\t\t\tconst detail = source.secondary ? fillTemplate(source.secondary, row).trim() : ''\n\t\t\tconst kind = source.label ? this.cnTranslate(source.label) : ''\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\tlabel: title,\n\t\t\t\tsecondary: [kind, detail].filter(Boolean).join(' · '),\n\t\t\t\tavatar: !source.icon,\n\t\t\t\ticon: source.icon || '',\n\t\t\t\tsource: source.id,\n\t\t\t\trow,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A Nextcloud contact as a picker option.\n\t\t *\n\t\t * @param {object} source The contacts source.\n\t\t * @param {object} contact The contactsmenu entry.\n\t\t * @return {object|null}\n\t\t */\n\t\tcontactOptionOf(source, contact) {\n\t\t\tconst id = contact && (contact.id || contact.uid)\n\t\t\tif (!id) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst mail = Array.isArray(contact.emailAddresses) ? contact.emailAddresses[0] : ''\n\t\t\tconst kind = source.label ? this.cnTranslate(source.label) : ''\n\t\t\treturn { id, label: contact.fullName || String(id), secondary: [kind, mail].filter(Boolean).join(' · '), avatar: true, source: source.id, row: contact }\n\t\t},\n\n\t\t/**\n\t\t * Search after typing rests.\n\t\t *\n\t\t * @param {string} query The typed text.\n\t\t */\n\t\tonSearch(query) {\n\t\t\tclearTimeout(this.timer)\n\t\t\tconst term = String(query || '').trim()\n\t\t\tif (term.length < this.minLength) {\n\t\t\t\tthis.options = this.selectedOption ? [this.selectedOption] : []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.timer = setTimeout(() => this.search(term), SEARCH_DELAY)\n\t\t},\n\n\t\t/**\n\t\t * Search every source at once; a source that fails adds nothing.\n\t\t *\n\t\t * @spec openspec/changes/party-picker/specs/party-picker/spec.md#requirement-one-search-field-over-several-kinds-of-party\n\t\t * @param {string} term The search term.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync search(term) {\n\t\t\tconst seq = ++this.searchSeq\n\t\t\tthis.searching = true\n\t\t\tconst sets = await Promise.all((this.sources || []).map(async (source) => {\n\t\t\t\ttry {\n\t\t\t\t\tif (source.type === 'contacts') {\n\t\t\t\t\t\tconst { data } = await axios.post(generateUrl('/contactsmenu/contacts'), { filter: term })\n\t\t\t\t\t\treturn (data && Array.isArray(data.contacts) ? data.contacts : []).map((c) => this.contactOptionOf(source, c))\n\t\t\t\t\t}\n\t\t\t\t\tif (!source.register || !source.schema) {\n\t\t\t\t\t\treturn []\n\t\t\t\t\t}\n\t\t\t\t\tconst { data } = await axios.get(generateUrl(`/apps/openregister/api/objects/${source.register}/${source.schema}`), { params: { _search: term, _limit: source.limit || 8 } })\n\t\t\t\t\treturn (data && Array.isArray(data.results) ? data.results : []).map((row) => this.optionOf(source, row))\n\t\t\t\t} catch {\n\t\t\t\t\treturn []\n\t\t\t\t}\n\t\t\t}))\n\t\t\tif (seq !== this.searchSeq) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.options = sets.flat().filter(Boolean)\n\t\t\tthis.searching = false\n\t\t},\n\n\t\t/**\n\t\t * Name a value the list does not hold: the first OpenRegister source\n\t\t * that has an object with that id.\n\t\t *\n\t\t * @param {string|number} id The id.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync lookUp(id) {\n\t\t\tfor (const source of (this.sources || []).filter((s) => s.register && s.schema && s.type !== 'contacts')) {\n\t\t\t\ttry {\n\t\t\t\t\tconst { data } = await axios.get(generateUrl(`/apps/openregister/api/objects/${source.register}/${source.schema}/${encodeURIComponent(id)}`))\n\t\t\t\t\tconst option = data ? this.optionOf(source, data) : null\n\t\t\t\t\tif (option && String(this.modelValue) === String(id)) {\n\t\t\t\t\t\tthis.known = option\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Not in this source; try the next.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Take the picked option: emit its id and the whole choice.\n\t\t *\n\t\t * @param {string|number|null} id The picked id.\n\t\t */\n\t\tonPick(id) {\n\t\t\tconst option = id === null ? null : this.options.find((o) => String(o.id) === String(id)) || null\n\t\t\tif (option) {\n\t\t\t\tthis.known = option\n\t\t\t}\n\t\t\t/** @event update:modelValue The chosen party's id, or null when cleared. */\n\t\t\tthis.$emit('update:modelValue', id)\n\t\t\t/** @event select The whole choice: `{ source, id, label, secondary, row }`, or null when cleared. */\n\t\t\tthis.$emit('select', option ? { source: option.source, id: option.id, label: option.label, secondary: option.secondary, row: option.row } : null)\n\t\t},\n\t},\n}\n</script>\n"],"names":[],"mappings":";;;;;;AAUE,IAAA,KAAK,EAAC,iBAAiB;AACtB,IAAA,UAAU,EAAE,QAAa,CAAA,cAAA;AACzB,IAAA,OAAO,EAAE,KAAO,CAAA,OAAA;AAChB,IAAA,UAAU,EAAE,MAAK,CAAA,UAAA;AACjB,IAAA,YAAY,EAAE,MAAM,CAAA,YAAA;AACpB,IAAA,WAAW,EAAE,MAAA,CAAA,WAAK,GAAA,QAAA,CAAA,WAAA,CAAA,MAAA,CAAA,WAAA,CAAA,GAAA,EAAA;AAClB,IAAA,OAAO,EAAE,KAAS,CAAA,SAAA;AAClB,IAAA,UAAU,EAAE,KAAI;AAChB,IAAA,SAAS,EAAE,MAAS,CAAA,SAAA;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnPartyPicker.vue3.js","sources":["../../../../src/components/CnPartyPicker/CnPartyPicker.vue?vue&type=template&id=7202e5ff&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-party-picker\">\n\t\t<CnSearchSelect\n\t\t\t:modelValue=\"selectedOption\"\n\t\t\t:options=\"options\"\n\t\t\t:inputLabel=\"inputLabel\"\n\t\t\t:labelOutside=\"labelOutside\"\n\t\t\t:placeholder=\"placeholder ? cnTranslate(placeholder) : ''\"\n\t\t\t:loading=\"searching\"\n\t\t\t:filterable=\"false\"\n\t\t\t:clearable=\"clearable\"\n\t\t\t:disabled=\"disabled\"\n\t\t\tdata-testid=\"cn-party-picker\"\n\t\t\t@search=\"onSearch\"\n\t\t\t@update:modelValue=\"onPick\" />\n\t\t<p v-if=\"refusal\"\n\t\t\tclass=\"cn-party-picker__refusal\"\n\t\t\trole=\"alert\"\n\t\t\tdata-testid=\"cn-party-picker-refusal\">\n\t\t\t{{ t('nextcloud-vue', 'This search could not be read.') }} {{ refusal }}\n\t\t</p>\n\t\t<p v-for=\"name in unavailable\"\n\t\t\t:key=\"name\"\n\t\t\tclass=\"cn-party-picker__unavailable\"\n\t\t\trole=\"status\"\n\t\t\tdata-testid=\"cn-party-picker-unavailable\">\n\t\t\t{{ t('nextcloud-vue', '{source} could not be searched, so this list may be incomplete.', { source: name }) }}\n\t\t</p>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { fillTemplate } from '../../utils/fillTemplate.js'\nimport { CnSearchSelect } from '../CnSearchSelect/index.js'\n\n/** How long typing rests before a search runs, in ms. */\nconst SEARCH_DELAY = 250\n\n/**\n * CnPartyPicker — one search field over several kinds of party: people and\n * organisations from OpenRegister register sets (BRP persons, KvK companies,\n * an app's own clients) and the reader's Nextcloud contacts, as rows with\n * initials, the name and a second line, in one list (board DqNieuweZaak\n * \"Aanvrager\": \"Henk Visser · Inwoner · Beukenlaan 3\", \"Visser Bouw B.V. ·\n * Organisatie · KvK …\"). Built on CnSearchSelect.\n *\n * `sources`: each `{ id, register, schema, label?, title?, secondary?,\n * limit?, icon? }` searches OpenRegister with `_search`; `title` and\n * `secondary` are templates filled from the row (default `{name}` / none),\n * `label` names the kind (\"Resident\") and starts the second line. A source\n * `{ id, type: \"contacts\", label? }` searches the reader's Nextcloud contacts.\n *\n * `update:modelValue` emits the chosen row's id (a contact has none and\n * emits its contact id); `select` emits the whole choice: `{ source, id,\n * label, secondary, row }`, so a host can store more than the id (a case's\n * initiator type, source id and display name). A `modelValue` the list does\n * not hold is shown by its `selectedLabel`, else looked up by id in the\n * OpenRegister sources.\n *\n * ```vue\n * <CnPartyPicker\n * v-model=\"case.requester\"\n * input-label=\"Requester\"\n * :sources=\"[\n * { id: 'person', register: 'dossiq', schema: 'brpPerson', label: 'Resident', title: '{firstName} {lastName}', secondary: '{residence.street} {residence.houseNumber}' },\n * { id: 'company', register: 'dossiq', schema: 'kvkCompany', label: 'Organisation', title: '{tradeName}', secondary: 'KvK {sourceId}' },\n * { id: 'contact', type: 'contacts', label: 'Contact' },\n * ]\"\n * @select=\"onRequester\" />\n * ```\n */\nexport default {\n\tname: 'CnPartyPicker',\n\n\tcomponents: { CnSearchSelect },\n\n\tinject: {\n\t\t/** The host app's translate (CnAppRoot `cnTranslate`). */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** The chosen party's id. */\n\t\tmodelValue: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Where to search: OpenRegister sets `{ id, register, schema, label?,\n\t\t * title?, secondary?, limit?, icon? }`, or `{ id, type: \"contacts\",\n\t\t * label? }` for the reader's Nextcloud contacts.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tsources: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The field's label. */\n\t\tinputLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Draw the label above the field. */\n\t\tlabelOutside: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Placeholder (\"Name, BSN, trade name or KvK number\"), through the host translate. */\n\t\tplaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The chosen party's name, shown before the list holds it. */\n\t\tselectedLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Offer a clear button. */\n\t\tclearable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Disable the field. */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** The fewest characters a search needs. */\n\t\tminLength: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\t},\n\n\temits: ['update:modelValue', 'select'],\n\n\tdata() {\n\t\treturn {\n\t\t\toptions: [],\n\t\t\tsearching: false,\n\t\t\tknown: null,\n\t\t\ttimer: null,\n\t\t\tsearchSeq: 0,\n\t\t\trefusal: '',\n\t\t\tunavailable: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The option for `modelValue`: in the list, else the known choice,\n\t\t * else one named by `selectedLabel`.\n\t\t *\n\t\t * @spec openspec/changes/party-picker/specs/party-picker/spec.md#requirement-one-search-field-over-several-kinds-of-party\n\t\t * @return {object|null}\n\t\t */\n\t\tselectedOption() {\n\t\t\tconst value = this.modelValue\n\t\t\tif (value === null || value === undefined || value === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst found = this.options.find((o) => String(o.id) === String(value))\n\t\t\tif (found) {\n\t\t\t\treturn found\n\t\t\t}\n\t\t\tif (this.known && String(this.known.id) === String(value)) {\n\t\t\t\treturn this.known\n\t\t\t}\n\t\t\treturn { id: value, label: this.selectedLabel || String(value), avatar: true }\n\t\t},\n\t},\n\n\twatch: {\n\t\tmodelValue: {\n\t\t\timmediate: true,\n\t\t\thandler(value) {\n\t\t\t\tif (value && !this.selectedLabel && !this.options.some((o) => String(o.id) === String(value))) {\n\t\t\t\t\tthis.lookUp(value)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tclearTimeout(this.timer)\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * A source's row as a picker option.\n\t\t *\n\t\t * @param {object} source The source.\n\t\t * @param {object} row The row.\n\t\t * @return {object|null} The option, or null without an id.\n\t\t */\n\t\toptionOf(source, row) {\n\t\t\tconst id = row && (row.id ?? row['@self']?.id ?? row.uuid)\n\t\t\tif (id === undefined || id === null || id === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst title = (source.title ? fillTemplate(source.title, row) : (row.name || row.title || row.displayName || '')).trim() || String(id)\n\t\t\tconst detail = source.secondary ? fillTemplate(source.secondary, row).trim() : ''\n\t\t\tconst kind = source.label ? this.cnTranslate(source.label) : ''\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\tlabel: title,\n\t\t\t\tsecondary: [kind, detail].filter(Boolean).join(' · '),\n\t\t\t\tavatar: !source.icon,\n\t\t\t\ticon: source.icon || '',\n\t\t\t\tsource: source.id,\n\t\t\t\trow,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A Nextcloud contact as a picker option.\n\t\t *\n\t\t * @param {object} source The contacts source.\n\t\t * @param {object} contact The contactsmenu entry.\n\t\t * @return {object|null}\n\t\t */\n\t\tcontactOptionOf(source, contact) {\n\t\t\tconst id = contact && (contact.id || contact.uid)\n\t\t\tif (!id) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst mail = Array.isArray(contact.emailAddresses) ? contact.emailAddresses[0] : ''\n\t\t\tconst kind = source.label ? this.cnTranslate(source.label) : ''\n\t\t\treturn { id, label: contact.fullName || String(id), secondary: [kind, mail].filter(Boolean).join(' · '), avatar: true, source: source.id, row: contact }\n\t\t},\n\n\t\t/**\n\t\t * Search after typing rests.\n\t\t *\n\t\t * @param {string} query The typed text.\n\t\t */\n\t\tonSearch(query) {\n\t\t\tclearTimeout(this.timer)\n\t\t\tconst term = String(query || '').trim()\n\t\t\tif (term.length < this.minLength) {\n\t\t\t\tthis.options = this.selectedOption ? [this.selectedOption] : []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.timer = setTimeout(() => this.search(term), SEARCH_DELAY)\n\t\t},\n\n\t\t/**\n\t\t * Search every source at once. A term a source refused (400) is said\n\t\t * under the field in its words; a source that could not be searched at\n\t\t * all is named, so an empty list never hides a missing source.\n\t\t *\n\t\t * @spec openspec/changes/party-picker/specs/party-picker/spec.md#requirement-one-search-field-over-several-kinds-of-party\n\t\t * @param {string} term The search term.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync search(term) {\n\t\t\tconst seq = ++this.searchSeq\n\t\t\tthis.searching = true\n\t\t\tlet refusal = ''\n\t\t\tconst unavailable = []\n\t\t\tconst sets = await Promise.all((this.sources || []).map(async (source) => {\n\t\t\t\ttry {\n\t\t\t\t\tif (source.type === 'contacts') {\n\t\t\t\t\t\tconst { data } = await axios.post(generateUrl('/contactsmenu/contacts'), { filter: term })\n\t\t\t\t\t\treturn (data && Array.isArray(data.contacts) ? data.contacts : []).map((c) => this.contactOptionOf(source, c))\n\t\t\t\t\t}\n\t\t\t\t\tif (!source.register || !source.schema) {\n\t\t\t\t\t\treturn []\n\t\t\t\t\t}\n\t\t\t\t\tconst { data } = await axios.get(generateUrl(`/apps/openregister/api/objects/${source.register}/${source.schema}`), { params: { _search: term, _limit: source.limit || 8 } })\n\t\t\t\t\treturn (data && Array.isArray(data.results) ? data.results : []).map((row) => this.optionOf(source, row))\n\t\t\t\t} catch (failure) {\n\t\t\t\t\t// OpenRegister refuses a term it cannot parse with a 400 rather\n\t\t\t\t\t// than answering nothing, so that refusal is said, not hidden.\n\t\t\t\t\tconst response = failure && failure.response\n\t\t\t\t\tif (response && response.status === 400) {\n\t\t\t\t\t\tconst body = response.data\n\t\t\t\t\t\trefusal = String((body && (body.message || body.error)) || (typeof body === 'string' ? body : '') || refusal)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tunavailable.push(source.label ? this.cnTranslate(source.label) : String(source.id || ''))\n\t\t\t\t\t}\n\t\t\t\t\treturn []\n\t\t\t\t}\n\t\t\t}))\n\t\t\tif (seq !== this.searchSeq) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.options = sets.flat().filter(Boolean)\n\t\t\tthis.refusal = refusal\n\t\t\tthis.unavailable = unavailable.filter(Boolean)\n\t\t\tthis.searching = false\n\t\t},\n\n\t\t/**\n\t\t * Name a value the list does not hold: the first OpenRegister source\n\t\t * that has an object with that id.\n\t\t *\n\t\t * @param {string|number} id The id.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync lookUp(id) {\n\t\t\tfor (const source of (this.sources || []).filter((s) => s.register && s.schema && s.type !== 'contacts')) {\n\t\t\t\ttry {\n\t\t\t\t\tconst { data } = await axios.get(generateUrl(`/apps/openregister/api/objects/${source.register}/${source.schema}/${encodeURIComponent(id)}`))\n\t\t\t\t\tconst option = data ? this.optionOf(source, data) : null\n\t\t\t\t\tif (option && String(this.modelValue) === String(id)) {\n\t\t\t\t\t\tthis.known = option\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Not in this source; try the next.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Take the picked option: emit its id and the whole choice.\n\t\t *\n\t\t * @param {string|number|null} id The picked id.\n\t\t */\n\t\tonPick(id) {\n\t\t\tconst option = id === null ? null : this.options.find((o) => String(o.id) === String(id)) || null\n\t\t\tif (option) {\n\t\t\t\tthis.known = option\n\t\t\t}\n\t\t\t/** @event update:modelValue The chosen party's id, or null when cleared. */\n\t\t\tthis.$emit('update:modelValue', id)\n\t\t\t/** @event select The whole choice: `{ source, id, label, secondary, row }`, or null when cleared. */\n\t\t\tthis.$emit('select', option ? { source: option.source, id: option.id, label: option.label, secondary: option.secondary, row: option.row } : null)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-party-picker__refusal {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-error-text, var(--color-error));\n}\n\n.cn-party-picker__unavailable {\n\tmargin: 4px 0 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_toDisplayString","_createElementBlock"],"mappings":";;AASM,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,iBAAc,GAAA;;;AAevB,EAAA,KAAE,EAAA,0BAAA;AACF,EAAA,IAAI,EAAC,OAAO;AACZ,EAAA,aAAU,EAAA,yBAAA;;;;;;;eAJoB,CAAA,yBAAA,EAAA;AAX7B,MAAA,UAAU,EAAE,QAAa,CAAA,cAAA;AACzB,MAAA,OAAO,EAAE,KAAO,CAAA,OAAA;AAChB,MAAA,UAAU,EAAE,MAAK,CAAA,UAAA;AACjB,MAAA,YAAY,EAAE,MAAM,CAAA,YAAA;AACpB,MAAA,WAAW,EAAE,MAAA,CAAA,WAAK,GAAA,QAAA,CAAA,WAAA,CAAA,MAAA,CAAA,WAAA,CAAA,GAAA,EAAA;AAClB,MAAA,OAAO,EAAE,KAAS,CAAA,SAAA;AAClB,MAAA,UAAU,EAAE,KAAI;AAChB,MAAA,SAAS,EAAE,MAAS,CAAA,SAAA;AACnB,MAAA,QAAO,EAAE,MAAI,CAAA,QAAA;AACf,MAAA,aAAW,EAAC,iBAAiB;AAC5B,MAAA,QAAM,EAAE,QAAE,CAAA,QAAA;AACV,MAAA,qBAAiB,EAAE,QAAM,CAAA,MAAA;;KAClB,KAAO,CAAA,OAAA;yCAKZ,eAAA,EAAAA,eAAA,CADA,UAAC,CAA+B,eAAA,EAAA,gCAAA,CAAA,CAAA,GAAA,GAAA,mBAAA,KAAA,CAAA,OAAA,CAAA,EAAA,CAAA,YAAA;;sBAEnCC,8CAAiB,KAAW,CAAA,WAAA,EAAA,CAAnB,IAAI,KAAA;2BAAbA;AACC,QAAA,GAAG,EAAE,IAAI;AACV,QAAA,KAAE,EAAA,8BAAA;AACF,QAAA,IAAG,EAAA,QAAA;;;;;;;;;"}
|
|
@@ -2,7 +2,7 @@ import script from './CnReportsPage.vue2.js';
|
|
|
2
2
|
import { render } from './CnReportsPage.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-6feca28f";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -4,6 +4,7 @@ import { defineAsyncComponent } from 'vue';
|
|
|
4
4
|
import __sfc_main from '../CnBuildiqEditButton/CnBuildiqEditButton.vue.js';
|
|
5
5
|
import __sfc_main$1 from '../CnIcon/CnIcon.vue.js';
|
|
6
6
|
import { useLook } from '../../composables/useLook.js';
|
|
7
|
+
import { headerActionAsLink } from '../../utils/actionLink.js';
|
|
7
8
|
import { resolveRegisteredHandler } from '../../utils/actionsDispatcher.js';
|
|
8
9
|
import { followLinkClick } from '../../utils/linkNavigation.js';
|
|
9
10
|
|
|
@@ -166,7 +167,11 @@ var script = {
|
|
|
166
167
|
* function, `navigate` (with `route`, and `params`), `emit`, `none`,
|
|
167
168
|
* or a registry name; `type: "navigate"` with a `target` URL opens
|
|
168
169
|
* that URL in a new tab. Every click also emits `header-action`
|
|
169
|
-
* `{ action, id }`, except for `handler: "none"`.
|
|
170
|
+
* `{ action, id }`, except for `handler: "none"`. An entry with `href`
|
|
171
|
+
* makes its button a real link that runs and emits nothing: an
|
|
172
|
+
* absolute `http(s)`, `mailto:`, `tel:` or `//host/...` URL as written,
|
|
173
|
+
* an instance path (leading `/`) through `generateUrl()`, in a new tab with
|
|
174
|
+
* `target: '_blank'`.
|
|
170
175
|
*
|
|
171
176
|
* @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
|
|
172
177
|
* @type {Array<object>|null}
|
|
@@ -292,24 +297,31 @@ var script = {
|
|
|
292
297
|
|
|
293
298
|
/**
|
|
294
299
|
* The declared header buttons that have a label and an action,
|
|
295
|
-
* translated, each with a stable key and a variant.
|
|
300
|
+
* translated, each with a stable key and a variant. A button naming a
|
|
301
|
+
* header action with `href` carries that `link`.
|
|
296
302
|
*
|
|
297
303
|
* @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
|
|
298
|
-
* @
|
|
304
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
305
|
+
* @return {Array<{key: string, label: string, action: string, variant: string, icon: string, link: ({href: string, target: string}|null)}>} The buttons.
|
|
299
306
|
*/
|
|
300
307
|
resolvedHeaderButtons() {
|
|
301
308
|
const declared = Array.isArray(this.config.headerButtons) ? this.config.headerButtons : [];
|
|
309
|
+
const actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : [];
|
|
302
310
|
return declared
|
|
303
311
|
.filter((button) => button && typeof button === 'object'
|
|
304
312
|
&& typeof button.action === 'string' && button.action !== ''
|
|
305
313
|
&& typeof button.label === 'string' && button.label !== '')
|
|
306
|
-
.map((button) =>
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
314
|
+
.map((button) => {
|
|
315
|
+
const entry = headerActionAsLink(actions.find((action) => action && action.id === button.action));
|
|
316
|
+
return {
|
|
317
|
+
key: String(button.id || button.action),
|
|
318
|
+
label: this.tr(button.label),
|
|
319
|
+
action: button.action,
|
|
320
|
+
variant: button.variant === 'primary' ? 'primary' : 'secondary',
|
|
321
|
+
icon: typeof button.icon === 'string' ? button.icon : '',
|
|
322
|
+
link: entry && typeof entry.href === 'string' && entry.href !== '' ? { href: entry.href, target: entry.target } : null,
|
|
323
|
+
}
|
|
324
|
+
})
|
|
313
325
|
},
|
|
314
326
|
|
|
315
327
|
/**
|
|
@@ -526,13 +538,18 @@ var script = {
|
|
|
526
538
|
|
|
527
539
|
/**
|
|
528
540
|
* A header button was clicked: run the `headerActions` entry it names
|
|
529
|
-
* and emit `header-action`, as an index page's header button does.
|
|
541
|
+
* and emit `header-action`, as an index page's header button does. A
|
|
542
|
+
* button that is a link is left to the browser.
|
|
530
543
|
*
|
|
531
544
|
* @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
|
|
532
|
-
* @
|
|
545
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
546
|
+
* @param {{action: string, link?: object}} button The resolved button.
|
|
533
547
|
* @return {void}
|
|
534
548
|
*/
|
|
535
549
|
onHeaderButton(button) {
|
|
550
|
+
if (button.link) {
|
|
551
|
+
return
|
|
552
|
+
}
|
|
536
553
|
const id = button.action;
|
|
537
554
|
const actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : [];
|
|
538
555
|
const entry = actions.find((action) => action && action.id === id) || null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnReportsPage.vue2.js","sources":["../../../../src/components/CnReportsPage/CnReportsPage.vue"],"sourcesContent":["<!--\n CnReportsPage — Manifest-driven Reports surface.\n\n v2 manifest page primitive for `type: \"reports\"`. One page holding every\n report an app offers, as cards, instead of a Reports SUBMENU with one entry\n per report.\n\n WHY THIS IS A PAGE AND NOT A MENU BRANCH. A submenu grows one entry per\n report and never shrinks. shillinq reached 96 report types; as menu children\n that is a scroll, not a navigation. Cards carry what a menu item cannot — a\n description, a category, a count — and a category filter turns 96 into a\n short list. shillinq proved the shape in `ReportingComplianceOverview`; this\n is that shape with the app-specific parts taken out, so every app declares it\n rather than copying 600 lines of Vue.\n\n Declare it as:\n\n { \"id\": \"Reports\", \"route\": \"/reports\", \"type\": \"reports\",\n \"config\": {\n \"categories\": { \"operational\": \"Operational\" },\n \"cards\": [\n { \"id\": \"Throughput\", \"label\": \"Processing time\",\n \"description\": \"How long cases take, by type.\",\n \"icon\": \"ChartLine\", \"category\": \"operational\",\n \"route\": \"Doorlooptijd\" }\n ]\n } }\n\n A card names a ROUTE by name, never a path: a path is editable per app and a\n card pointing at a stale one is a dead end that still looks like a report.\n\n Spec: hydra openspec/architecture/adr-112-reports-are-one-page.md (ConductionNL/hydra#640).\n-->\n<template>\n\t<div class=\"cn-reports-page\" :class=\"{ 'cn-reports-page--board': isBoard }\">\n\t\t<div class=\"cn-reports-page__header\">\n\t\t\t<!-- The header buttons (`headerButtons`) beside the title, as the\n\t\t\t board's \"Downloaden\" and the buildiq square (board DcRapportages).\n\t\t\t Without them, and outside the board look, the title stands alone\n\t\t\t as before. -->\n\t\t\t<div v-if=\"headerButtonsRow.length > 0\" class=\"cn-reports-page__heading-row\">\n\t\t\t\t<h2 class=\"cn-reports-page__title\">\n\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t</h2>\n\t\t\t\t<div class=\"cn-reports-page__header-buttons\" data-testid=\"cn-reports-header-buttons\">\n\t\t\t\t\t<template v-for=\"button in headerButtonsRow\" :key=\"button.key\">\n\t\t\t\t\t\t<CnBuildiqEditButton v-if=\"button.action === '__buildiq'\" />\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t:variant=\"button.variant\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-reports-header-button-${button.key}`\"\n\t\t\t\t\t\t\t@click=\"onHeaderButton(button)\">\n\t\t\t\t\t\t\t<template v-if=\"button.icon\" #icon>\n\t\t\t\t\t\t\t\t<CnIcon :name=\"button.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ button.label }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</template>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<h2 v-else class=\"cn-reports-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-reports-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<!-- The board look (board DcRapportages): one section per category,\n\t\t headed by its label, instead of the category filter; each card\n\t\t draws its icon in a tile. -->\n\t\t<template v-if=\"isBoard\">\n\t\t\t<p v-if=\"resolvedCards.length === 0\"\n\t\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t\t{{ resolvedEmptyLabel }}\n\t\t\t</p>\n\t\t\t<section v-for=\"section in boardSections\"\n\t\t\t\t:key=\"section.key\"\n\t\t\t\tclass=\"cn-reports-page__section\"\n\t\t\t\tdata-testid=\"cn-reports-section\">\n\t\t\t\t<h3 v-if=\"section.label\" class=\"cn-reports-page__section-title\">\n\t\t\t\t\t{{ section.label }}\n\t\t\t\t</h3>\n\t\t\t\t<ul class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t\t\t<li v-for=\"card in section.cards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t\t\t<span v-if=\"card.icon\" class=\"cn-reports-page__card-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t<CnIcon :name=\"card.icon\" :size=\"22\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span class=\"cn-reports-page__card-text\">\n\t\t\t\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\t\t</template>\n\n\t\t<div v-if=\"!isBoard && categoryOptions.length > 1\" class=\"cn-reports-page__filter\">\n\t\t\t<label class=\"cn-reports-page__filter-label\" :for=\"filterId\">\n\t\t\t\t{{ resolvedCategoryLabel }}\n\t\t\t</label>\n\t\t\t<select :id=\"filterId\"\n\t\t\t\tv-model=\"activeCategory\"\n\t\t\t\tclass=\"cn-reports-page__filter-select\"\n\t\t\t\tdata-testid=\"cn-reports-category\">\n\t\t\t\t<option v-for=\"option in categoryOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:value=\"option.value\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<p v-if=\"!isBoard && visibleCards.length === 0\"\n\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<ul v-else-if=\"!isBoard\" class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t<li v-for=\"card in visibleCards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"categoryName(card)\" class=\"cn-reports-page__card-category\">\n\t\t\t\t\t\t{{ categoryName(card) }}\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<!-- `widgets` + `layout`: a grid of dashboard widgets under the cards,\n\t\t such as a highlighted report with stat tiles and a chart. -->\n\t\t<div v-if=\"sectionWidgets.length > 0\" class=\"cn-reports-page__widgets\" data-testid=\"cn-reports-widgets\">\n\t\t\t<CnDashboardPage\n\t\t\t\t:widgets=\"sectionWidgets\"\n\t\t\t\t:layout=\"sectionLayout\"\n\t\t\t\t:showHeader=\"false\" />\n\t\t</div>\n\t</div>\n</template>\n\n<script>\n/**\n * The Reports page: one surface listing an app's reports as cards.\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton } from '@nextcloud/vue'\nimport { defineAsyncComponent } from 'vue'\nimport CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport { useLook } from '../../composables/useLook.js'\nimport { resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'\nimport { followLinkClick } from '../../utils/linkNavigation.js'\n\nexport default {\n\tname: 'CnReportsPage',\n\n\tcomponents: {\n\t\t// Loaded only for a page that declares widgets: a cards-only reports\n\t\t// page does not pull the dashboard in.\n\t\tCnDashboardPage: defineAsyncComponent(() => import('../CnDashboardPage/CnDashboardPage.vue').then((m) => m.default)),\n\t\tCnBuildiqEditButton,\n\t\tCnIcon,\n\t\tNcButton,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * The v2 registry and the legacy customComponents map, provided by\n\t\t * CnAppRoot: a header action's named handler resolves against them.\n\t\t */\n\t\tcnRegistry: { default: () => ({}) },\n\t\tcnCustomComponents: { default: () => ({}) },\n\t\t/**\n\t\t * The consuming app's translate function, provided by CnAppRoot.\n\t\t * Defaults to identity so the page still renders outside one.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"reports\"` page arrived here with the\n\t\t * default `{}` — `config` resolved to `{}`, `cards` to `[]`, and every\n\t\t * reports page in the fleet rendered its empty state while its manifest\n\t\t * declared cards. Kept because a host that renders this component\n\t\t * directly can still hand it the whole page; the flattened props below\n\t\t * are what the renderer actually supplies, and they win.\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The report cards, as CnPageRenderer flattens them out of `config`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Category key/label pairs, flattened out of `config`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The heading, lifted from the page by the renderer.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the \"all categories\" filter option.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tallCategoriesLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the category filter control.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcategoryLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Text shown when the active filter admits no card.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Dashboard widgets drawn under the report cards (`config.widgets`),\n\t\t * placed by `layout`, as on a `type: \"dashboard\"` page.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\twidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Grid placement of `widgets` (`config.layout`): `{ id, widgetId,\n\t\t * gridX, gridY, gridWidth, gridHeight }` per widget. Without it each\n\t\t * widget takes a full row in the order declared.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tlayout: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Buttons beside the title (`config.headerButtons`), as the board's\n\t\t * \"Downloaden\". Each is `{ label, action, variant?, icon?, id? }`:\n\t\t * `action` is the id of a `headerActions` entry, which says what the\n\t\t * click does. Under the board look the buildiq square sits between the\n\t\t * secondary buttons and the primary one. Empty (the default) changes\n\t\t * nothing.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @type {Array<{label: string, action: string, variant?: ('primary'|'secondary'), icon?: string, id?: string}>|null}\n\t\t */\n\t\theaderButtons: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The page's header actions (`config.headerActions`), in the shape of\n\t\t * an index page's: a header button names one by id. `handler` is a\n\t\t * function, `navigate` (with `route`, and `params`), `emit`, `none`,\n\t\t * or a registry name; `type: \"navigate\"` with a `target` URL opens\n\t\t * that URL in a new tab. Every click also emits `header-action`\n\t\t * `{ action, id }`, except for `handler: \"none\"`.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\theaderActions: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The look to draw in: `board` or `nextcloud`. Absent: the look\n\t\t * CnAppRoot provides (`cnLook`), else `nextcloud`.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tlook: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['header-action'],\n\n\tsetup(props) {\n\t\tconst { isBoard } = useLook(props)\n\t\treturn { isBoard }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveCategory: 'all',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The translate function actually in force.\n\t\t *\n\t\t * @return {(key: string) => string} The translator.\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The declared config, always an object, from whichever direction it\n\t\t * arrived.\n\t\t *\n\t\t * TWO CALLERS, TWO SHAPES. CnPageRenderer flattens `page.config.*` into\n\t\t * individual props and passes NO `page`, so under the renderer the keys\n\t\t * arrive as props. A host that mounts this component itself hands it the\n\t\t * whole `page`. Reading only `page.config` is what made every dispatched\n\t\t * reports page render empty.\n\t\t *\n\t\t * Props win where both are present: they are the nearer declaration, and\n\t\t * the renderer has already resolved route sentinels in them.\n\t\t *\n\t\t * @return {object} The config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of [\n\t\t\t\t'cards',\n\t\t\t\t'categories',\n\t\t\t\t'description',\n\t\t\t\t'title',\n\t\t\t\t'allCategoriesLabel',\n\t\t\t\t'categoryLabel',\n\t\t\t\t'emptyLabel',\n\t\t\t\t'widgets',\n\t\t\t\t'layout',\n\t\t\t\t'headerButtons',\n\t\t\t\t'headerActions',\n\t\t\t]) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The widgets under the cards.\n\t\t *\n\t\t * @return {Array<object>} The declared widgets, or none.\n\t\t * @spec openspec/changes/screens-reports-page-widgets/specs/reports-page/spec.md\n\t\t */\n\t\tsectionWidgets() {\n\t\t\tconst widgets = this.config.widgets\n\t\t\treturn Array.isArray(widgets) ? widgets.filter((w) => w && typeof w === 'object' && w.id) : []\n\t\t},\n\n\t\t/**\n\t\t * Where the widgets go: the declared `layout`, else one full row per\n\t\t * widget in the order declared.\n\t\t *\n\t\t * @return {Array<object>} The placements.\n\t\t * @spec openspec/changes/screens-reports-page-widgets/specs/reports-page/spec.md\n\t\t */\n\t\tsectionLayout() {\n\t\t\tif (Array.isArray(this.config.layout) && this.config.layout.length > 0) {\n\t\t\t\treturn this.config.layout\n\t\t\t}\n\t\t\tlet y = 0\n\t\t\treturn this.sectionWidgets.map((widget, index) => {\n\t\t\t\tconst placement = { id: String(index + 1), widgetId: widget.id, gridX: 0, gridY: y, gridWidth: 12, gridHeight: 4 }\n\t\t\t\ty += 4\n\t\t\t\treturn placement\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The declared header buttons that have a label and an action,\n\t\t * translated, each with a stable key and a variant.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @return {Array<{key: string, label: string, action: string, variant: string, icon: string}>} The buttons.\n\t\t */\n\t\tresolvedHeaderButtons() {\n\t\t\tconst declared = Array.isArray(this.config.headerButtons) ? this.config.headerButtons : []\n\t\t\treturn declared\n\t\t\t\t.filter((button) => button && typeof button === 'object'\n\t\t\t\t\t&& typeof button.action === 'string' && button.action !== ''\n\t\t\t\t\t&& typeof button.label === 'string' && button.label !== '')\n\t\t\t\t.map((button) => ({\n\t\t\t\t\tkey: String(button.id || button.action),\n\t\t\t\t\tlabel: this.tr(button.label),\n\t\t\t\t\taction: button.action,\n\t\t\t\t\tvariant: button.variant === 'primary' ? 'primary' : 'secondary',\n\t\t\t\t\ticon: typeof button.icon === 'string' ? button.icon : '',\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The header row in the order it renders. Without the board look:\n\t\t * the declared buttons in the declared order. Under the board look:\n\t\t * the secondary buttons, the buildiq square, then the primary ones,\n\t\t * as the board draws \"Downloaden\" and the square. No declared button\n\t\t * leaves the header as it was, without the square.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @return {Array<object>} The buttons; the square is `{ key: '__buildiq', action: '__buildiq' }`.\n\t\t */\n\t\theaderButtonsRow() {\n\t\t\tconst buttons = this.resolvedHeaderButtons\n\t\t\tif (buttons.length === 0 || !this.isBoard) {\n\t\t\t\treturn buttons\n\t\t\t}\n\t\t\tconst square = { key: '__buildiq', action: '__buildiq', variant: 'secondary', label: '', icon: '' }\n\t\t\treturn [\n\t\t\t\t...buttons.filter((button) => button.variant !== 'primary'),\n\t\t\t\tsquare,\n\t\t\t\t...buttons.filter((button) => button.variant === 'primary'),\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The heading.\n\t\t *\n\t\t * @return {string} The title.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\tconst declared = this.config.title || (this.page && this.page.title)\n\n\t\t\treturn declared ? this.tr(declared) : t('nextcloud-vue', 'Reports')\n\t\t},\n\n\t\t/**\n\t\t * The optional lead paragraph.\n\t\t *\n\t\t * @return {string} The description.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The declared cards, ignoring any that name no route.\n\t\t *\n\t\t * A card with no route is a card that cannot be opened. Rendering it\n\t\t * would put a report on the page that silently does nothing, which is\n\t\t * worse than leaving it off.\n\t\t *\n\t\t * @return {Array<object>} The usable cards.\n\t\t */\n\t\tresolvedCards() {\n\t\t\tconst declared = this.config.cards\n\t\t\tif (!Array.isArray(declared)) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.route && card.label)\n\t\t\t\t.map((card) => ({\n\t\t\t\t\t...card,\n\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Category value/label pairs, offered only for categories that hold a\n\t\t * card. A filter listing empty categories teaches the reader that the\n\t\t * app has reports it does not have.\n\t\t *\n\t\t * @return {Array<object>} The options.\n\t\t */\n\t\tcategoryOptions() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst used = new Set(this.resolvedCards.map((card) => card.category).filter(Boolean))\n\t\t\tconst options = [{ value: 'all', label: this.allLabel }]\n\n\t\t\tObject.keys(declared)\n\t\t\t\t.filter((key) => used.has(key))\n\t\t\t\t.forEach((key) => options.push({ value: key, label: this.tr(declared[key]) }))\n\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * The board look's sections: the cards without a category first, under\n\t\t * no heading, then one section per declared category that holds a\n\t\t * card, in the declared order, headed by its label.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-page-board-look/specs/reports-page/spec.md#requirement-the-reports-page-takes-the-board-look\n\t\t * @return {Array<{key: string, label: string, cards: Array<object>}>} The sections.\n\t\t */\n\t\tboardSections() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst sections = []\n\t\t\tconst loose = this.resolvedCards.filter((card) => !card.category || !Object.hasOwn(declared, card.category))\n\t\t\tif (loose.length > 0) {\n\t\t\t\tsections.push({ key: '__none', label: '', cards: loose })\n\t\t\t}\n\t\t\tfor (const key of Object.keys(declared)) {\n\t\t\t\tconst cards = this.resolvedCards.filter((card) => card.category === key)\n\t\t\t\tif (cards.length > 0) {\n\t\t\t\t\tsections.push({ key, label: this.tr(declared[key]), cards })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn sections\n\t\t},\n\n\t\t/**\n\t\t * The cards the active filter admits.\n\t\t *\n\t\t * @return {Array<object>} The visible cards.\n\t\t */\n\t\tvisibleCards() {\n\t\t\tif (this.activeCategory === 'all') {\n\t\t\t\treturn this.resolvedCards\n\t\t\t}\n\n\t\t\treturn this.resolvedCards.filter((card) => card.category === this.activeCategory)\n\t\t},\n\n\t\t/**\n\t\t * A stable id for the filter control's label association.\n\t\t *\n\t\t * @return {string} The id.\n\t\t */\n\t\tfilterId() {\n\t\t\treturn `cn-reports-category-${(this.page && this.page.id) || 'page'}`\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The \"all categories\" option label.\n\t\t */\n\t\tallLabel() {\n\t\t\treturn this.config.allCategoriesLabel\n\t\t\t\t? this.tr(this.config.allCategoriesLabel)\n\t\t\t\t: t('nextcloud-vue', 'All categories')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The filter's label.\n\t\t */\n\t\tresolvedCategoryLabel() {\n\t\t\treturn this.config.categoryLabel\n\t\t\t\t? this.tr(this.config.categoryLabel)\n\t\t\t\t: t('nextcloud-vue', 'Category')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The empty-state text.\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel\n\t\t\t\t? this.tr(this.config.emptyLabel)\n\t\t\t\t: t('nextcloud-vue', 'No reports in this category.')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest-declared string through the app's own\n\t\t * translate function.\n\t\t *\n\t\t * The manifest is data the renderer walks, not source the extractor\n\t\t * reads, so every string it carries has to be handed to the app's\n\t\t * translator explicitly. A card rendered straight from the manifest\n\t\t * shows its source language whatever locale the reader is in.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translated string, or the input unchanged.\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\treturn this.effectiveTranslate(value)\n\t\t},\n\n\t\t/**\n\t\t * The human label for a card's category, if it has one.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The label, or an empty string.\n\t\t */\n\t\tcategoryName(card) {\n\t\t\tconst declared = this.config.categories || {}\n\n\t\t\treturn this.tr((card.category && declared[card.category]) || '')\n\t\t},\n\n\t\t/**\n\t\t * A real href for the card, so it is a link a reader can middle-click\n\t\t * or open in a new tab rather than a div that only answers to a click.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The href.\n\t\t */\n\t\threfFor(card) {\n\t\t\tif (!this.$router) {\n\t\t\t\treturn '#'\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn this.$router.resolve({ name: card.route }).href\n\t\t\t} catch {\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A header button was clicked: run the `headerActions` entry it names\n\t\t * and emit `header-action`, as an index page's header button does.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @param {{action: string}} button The resolved button.\n\t\t * @return {void}\n\t\t */\n\t\tonHeaderButton(button) {\n\t\t\tconst id = button.action\n\t\t\tconst actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : []\n\t\t\tconst entry = actions.find((action) => action && action.id === id) || null\n\t\t\tconst handler = entry ? entry.handler : undefined\n\t\t\tif (handler === 'none') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof handler === 'function') {\n\t\t\t\thandler({ actionId: id })\n\t\t\t} else if (handler === 'navigate' && entry.route) {\n\t\t\t\tconst params = (entry.params && typeof entry.params === 'object') ? entry.params : null\n\t\t\t\tif (this.$router && typeof this.$router.push === 'function') {\n\t\t\t\t\tthis.$router.push(params ? { name: entry.route, params } : { name: entry.route })\n\t\t\t\t}\n\t\t\t} else if (entry && entry.type === 'navigate' && typeof entry.target === 'string' && entry.target !== '') {\n\t\t\t\twindow.open(entry.target, '_blank', 'noopener,noreferrer')\n\t\t\t} else if (typeof handler === 'string' && handler !== 'emit') {\n\t\t\t\tconst resolved = resolveRegisteredHandler(handler, this.cnRegistry, this.cnCustomComponents)\n\t\t\t\tif (typeof resolved === 'function') {\n\t\t\t\t\tresolved({ actionId: id })\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * A header button was clicked (`headerButtons`), after its\n\t\t\t * `headerActions` entry ran; not for `handler: \"none\"`.\n\t\t\t *\n\t\t\t * @event header-action\n\t\t\t * @property {{action: string, id: string}} payload The id of the `headerActions` entry the button names, as `action` and `id`.\n\t\t\t */\n\t\t\tthis.$emit('header-action', { action: id, id })\n\t\t},\n\n\t\t/**\n\t\t * Navigate by route NAME on a plain click; a modified or middle click\n\t\t * is left to the browser so it can open the card's href elsewhere.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t */\n\t\topen(card, event) {\n\t\t\tfollowLinkClick(event, { name: card.route }, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-reports-page {\n\tpadding: 16px;\n}\n\n/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n positioned at the left edge of .app-content) plus 12px breathing room, as\n CnPageHeader does. Only the header shifts; the body keeps the full width. */\n.cn-reports-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-reports-page__title {\n\tmargin: 0 0 4px;\n}\n\n.cn-reports-page__heading-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-start;\n\tjustify-content: space-between;\n\tgap: 12px 24px;\n}\n\n.cn-reports-page__heading-row .cn-reports-page__title {\n\tflex: 1 1 320px;\n\tmin-width: 0;\n}\n\n.cn-reports-page__header-buttons {\n\tdisplay: flex;\n\tflex: none;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 10px;\n}\n\n.cn-reports-page__description {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__filter {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 16px;\n}\n\n.cn-reports-page__filter-select {\n\tmin-width: 220px;\n}\n\n.cn-reports-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 12px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-reports-page__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\theight: 100%;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-reports-page__card:hover,\n.cn-reports-page__card:focus {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-reports-page__card-title {\n\tfont-weight: bold;\n}\n\n.cn-reports-page__card-description,\n.cn-reports-page__card-category {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-reports-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__widgets {\n\tmargin-top: 20px;\n}\n</style>\n"],"names":["CnBuildiqEditButton","CnIcon","t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnReportsPage.vue2.js","sources":["../../../../src/components/CnReportsPage/CnReportsPage.vue"],"sourcesContent":["<!--\n CnReportsPage — Manifest-driven Reports surface.\n\n v2 manifest page primitive for `type: \"reports\"`. One page holding every\n report an app offers, as cards, instead of a Reports SUBMENU with one entry\n per report.\n\n WHY THIS IS A PAGE AND NOT A MENU BRANCH. A submenu grows one entry per\n report and never shrinks. shillinq reached 96 report types; as menu children\n that is a scroll, not a navigation. Cards carry what a menu item cannot — a\n description, a category, a count — and a category filter turns 96 into a\n short list. shillinq proved the shape in `ReportingComplianceOverview`; this\n is that shape with the app-specific parts taken out, so every app declares it\n rather than copying 600 lines of Vue.\n\n Declare it as:\n\n { \"id\": \"Reports\", \"route\": \"/reports\", \"type\": \"reports\",\n \"config\": {\n \"categories\": { \"operational\": \"Operational\" },\n \"cards\": [\n { \"id\": \"Throughput\", \"label\": \"Processing time\",\n \"description\": \"How long cases take, by type.\",\n \"icon\": \"ChartLine\", \"category\": \"operational\",\n \"route\": \"Doorlooptijd\" }\n ]\n } }\n\n A card names a ROUTE by name, never a path: a path is editable per app and a\n card pointing at a stale one is a dead end that still looks like a report.\n\n Spec: hydra openspec/architecture/adr-112-reports-are-one-page.md (ConductionNL/hydra#640).\n-->\n<template>\n\t<div class=\"cn-reports-page\" :class=\"{ 'cn-reports-page--board': isBoard }\">\n\t\t<div class=\"cn-reports-page__header\">\n\t\t\t<!-- The header buttons (`headerButtons`) beside the title, as the\n\t\t\t board's \"Downloaden\" and the buildiq square (board DcRapportages).\n\t\t\t Without them, and outside the board look, the title stands alone\n\t\t\t as before. -->\n\t\t\t<div v-if=\"headerButtonsRow.length > 0\" class=\"cn-reports-page__heading-row\">\n\t\t\t\t<h2 class=\"cn-reports-page__title\">\n\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t</h2>\n\t\t\t\t<div class=\"cn-reports-page__header-buttons\" data-testid=\"cn-reports-header-buttons\">\n\t\t\t\t\t<template v-for=\"button in headerButtonsRow\" :key=\"button.key\">\n\t\t\t\t\t\t<CnBuildiqEditButton v-if=\"button.action === '__buildiq'\" />\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t:variant=\"button.variant\"\n\t\t\t\t\t\t\t:href=\"button.link ? button.link.href : undefined\"\n\t\t\t\t\t\t\t:target=\"button.link ? button.link.target : undefined\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-reports-header-button-${button.key}`\"\n\t\t\t\t\t\t\t@click=\"onHeaderButton(button)\">\n\t\t\t\t\t\t\t<template v-if=\"button.icon\" #icon>\n\t\t\t\t\t\t\t\t<CnIcon :name=\"button.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ button.label }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</template>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<h2 v-else class=\"cn-reports-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-reports-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<!-- The board look (board DcRapportages): one section per category,\n\t\t headed by its label, instead of the category filter; each card\n\t\t draws its icon in a tile. -->\n\t\t<template v-if=\"isBoard\">\n\t\t\t<p v-if=\"resolvedCards.length === 0\"\n\t\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t\t{{ resolvedEmptyLabel }}\n\t\t\t</p>\n\t\t\t<section v-for=\"section in boardSections\"\n\t\t\t\t:key=\"section.key\"\n\t\t\t\tclass=\"cn-reports-page__section\"\n\t\t\t\tdata-testid=\"cn-reports-section\">\n\t\t\t\t<h3 v-if=\"section.label\" class=\"cn-reports-page__section-title\">\n\t\t\t\t\t{{ section.label }}\n\t\t\t\t</h3>\n\t\t\t\t<ul class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t\t\t<li v-for=\"card in section.cards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t\t\t<span v-if=\"card.icon\" class=\"cn-reports-page__card-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t<CnIcon :name=\"card.icon\" :size=\"22\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span class=\"cn-reports-page__card-text\">\n\t\t\t\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\t\t</template>\n\n\t\t<div v-if=\"!isBoard && categoryOptions.length > 1\" class=\"cn-reports-page__filter\">\n\t\t\t<label class=\"cn-reports-page__filter-label\" :for=\"filterId\">\n\t\t\t\t{{ resolvedCategoryLabel }}\n\t\t\t</label>\n\t\t\t<select :id=\"filterId\"\n\t\t\t\tv-model=\"activeCategory\"\n\t\t\t\tclass=\"cn-reports-page__filter-select\"\n\t\t\t\tdata-testid=\"cn-reports-category\">\n\t\t\t\t<option v-for=\"option in categoryOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:value=\"option.value\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<p v-if=\"!isBoard && visibleCards.length === 0\"\n\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<ul v-else-if=\"!isBoard\" class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t<li v-for=\"card in visibleCards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"categoryName(card)\" class=\"cn-reports-page__card-category\">\n\t\t\t\t\t\t{{ categoryName(card) }}\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<!-- `widgets` + `layout`: a grid of dashboard widgets under the cards,\n\t\t such as a highlighted report with stat tiles and a chart. -->\n\t\t<div v-if=\"sectionWidgets.length > 0\" class=\"cn-reports-page__widgets\" data-testid=\"cn-reports-widgets\">\n\t\t\t<CnDashboardPage\n\t\t\t\t:widgets=\"sectionWidgets\"\n\t\t\t\t:layout=\"sectionLayout\"\n\t\t\t\t:showHeader=\"false\" />\n\t\t</div>\n\t</div>\n</template>\n\n<script>\n/**\n * The Reports page: one surface listing an app's reports as cards.\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton } from '@nextcloud/vue'\nimport { defineAsyncComponent } from 'vue'\nimport CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport { useLook } from '../../composables/useLook.js'\nimport { headerActionAsLink } from '../../utils/actionLink.js'\nimport { resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'\nimport { followLinkClick } from '../../utils/linkNavigation.js'\n\nexport default {\n\tname: 'CnReportsPage',\n\n\tcomponents: {\n\t\t// Loaded only for a page that declares widgets: a cards-only reports\n\t\t// page does not pull the dashboard in.\n\t\tCnDashboardPage: defineAsyncComponent(() => import('../CnDashboardPage/CnDashboardPage.vue').then((m) => m.default)),\n\t\tCnBuildiqEditButton,\n\t\tCnIcon,\n\t\tNcButton,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * The v2 registry and the legacy customComponents map, provided by\n\t\t * CnAppRoot: a header action's named handler resolves against them.\n\t\t */\n\t\tcnRegistry: { default: () => ({}) },\n\t\tcnCustomComponents: { default: () => ({}) },\n\t\t/**\n\t\t * The consuming app's translate function, provided by CnAppRoot.\n\t\t * Defaults to identity so the page still renders outside one.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"reports\"` page arrived here with the\n\t\t * default `{}` — `config` resolved to `{}`, `cards` to `[]`, and every\n\t\t * reports page in the fleet rendered its empty state while its manifest\n\t\t * declared cards. Kept because a host that renders this component\n\t\t * directly can still hand it the whole page; the flattened props below\n\t\t * are what the renderer actually supplies, and they win.\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The report cards, as CnPageRenderer flattens them out of `config`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Category key/label pairs, flattened out of `config`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The heading, lifted from the page by the renderer.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the \"all categories\" filter option.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tallCategoriesLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the category filter control.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcategoryLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Text shown when the active filter admits no card.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Dashboard widgets drawn under the report cards (`config.widgets`),\n\t\t * placed by `layout`, as on a `type: \"dashboard\"` page.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\twidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Grid placement of `widgets` (`config.layout`): `{ id, widgetId,\n\t\t * gridX, gridY, gridWidth, gridHeight }` per widget. Without it each\n\t\t * widget takes a full row in the order declared.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tlayout: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Buttons beside the title (`config.headerButtons`), as the board's\n\t\t * \"Downloaden\". Each is `{ label, action, variant?, icon?, id? }`:\n\t\t * `action` is the id of a `headerActions` entry, which says what the\n\t\t * click does. Under the board look the buildiq square sits between the\n\t\t * secondary buttons and the primary one. Empty (the default) changes\n\t\t * nothing.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @type {Array<{label: string, action: string, variant?: ('primary'|'secondary'), icon?: string, id?: string}>|null}\n\t\t */\n\t\theaderButtons: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The page's header actions (`config.headerActions`), in the shape of\n\t\t * an index page's: a header button names one by id. `handler` is a\n\t\t * function, `navigate` (with `route`, and `params`), `emit`, `none`,\n\t\t * or a registry name; `type: \"navigate\"` with a `target` URL opens\n\t\t * that URL in a new tab. Every click also emits `header-action`\n\t\t * `{ action, id }`, except for `handler: \"none\"`. An entry with `href`\n\t\t * makes its button a real link that runs and emits nothing: an\n\t\t * absolute `http(s)`, `mailto:`, `tel:` or `//host/...` URL as written,\n\t\t * an instance path (leading `/`) through `generateUrl()`, in a new tab with\n\t\t * `target: '_blank'`.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\theaderActions: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The look to draw in: `board` or `nextcloud`. Absent: the look\n\t\t * CnAppRoot provides (`cnLook`), else `nextcloud`.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tlook: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['header-action'],\n\n\tsetup(props) {\n\t\tconst { isBoard } = useLook(props)\n\t\treturn { isBoard }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveCategory: 'all',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The translate function actually in force.\n\t\t *\n\t\t * @return {(key: string) => string} The translator.\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The declared config, always an object, from whichever direction it\n\t\t * arrived.\n\t\t *\n\t\t * TWO CALLERS, TWO SHAPES. CnPageRenderer flattens `page.config.*` into\n\t\t * individual props and passes NO `page`, so under the renderer the keys\n\t\t * arrive as props. A host that mounts this component itself hands it the\n\t\t * whole `page`. Reading only `page.config` is what made every dispatched\n\t\t * reports page render empty.\n\t\t *\n\t\t * Props win where both are present: they are the nearer declaration, and\n\t\t * the renderer has already resolved route sentinels in them.\n\t\t *\n\t\t * @return {object} The config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of [\n\t\t\t\t'cards',\n\t\t\t\t'categories',\n\t\t\t\t'description',\n\t\t\t\t'title',\n\t\t\t\t'allCategoriesLabel',\n\t\t\t\t'categoryLabel',\n\t\t\t\t'emptyLabel',\n\t\t\t\t'widgets',\n\t\t\t\t'layout',\n\t\t\t\t'headerButtons',\n\t\t\t\t'headerActions',\n\t\t\t]) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The widgets under the cards.\n\t\t *\n\t\t * @return {Array<object>} The declared widgets, or none.\n\t\t * @spec openspec/changes/screens-reports-page-widgets/specs/reports-page/spec.md\n\t\t */\n\t\tsectionWidgets() {\n\t\t\tconst widgets = this.config.widgets\n\t\t\treturn Array.isArray(widgets) ? widgets.filter((w) => w && typeof w === 'object' && w.id) : []\n\t\t},\n\n\t\t/**\n\t\t * Where the widgets go: the declared `layout`, else one full row per\n\t\t * widget in the order declared.\n\t\t *\n\t\t * @return {Array<object>} The placements.\n\t\t * @spec openspec/changes/screens-reports-page-widgets/specs/reports-page/spec.md\n\t\t */\n\t\tsectionLayout() {\n\t\t\tif (Array.isArray(this.config.layout) && this.config.layout.length > 0) {\n\t\t\t\treturn this.config.layout\n\t\t\t}\n\t\t\tlet y = 0\n\t\t\treturn this.sectionWidgets.map((widget, index) => {\n\t\t\t\tconst placement = { id: String(index + 1), widgetId: widget.id, gridX: 0, gridY: y, gridWidth: 12, gridHeight: 4 }\n\t\t\t\ty += 4\n\t\t\t\treturn placement\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The declared header buttons that have a label and an action,\n\t\t * translated, each with a stable key and a variant. A button naming a\n\t\t * header action with `href` carries that `link`.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing\n\t\t * @return {Array<{key: string, label: string, action: string, variant: string, icon: string, link: ({href: string, target: string}|null)}>} The buttons.\n\t\t */\n\t\tresolvedHeaderButtons() {\n\t\t\tconst declared = Array.isArray(this.config.headerButtons) ? this.config.headerButtons : []\n\t\t\tconst actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : []\n\t\t\treturn declared\n\t\t\t\t.filter((button) => button && typeof button === 'object'\n\t\t\t\t\t&& typeof button.action === 'string' && button.action !== ''\n\t\t\t\t\t&& typeof button.label === 'string' && button.label !== '')\n\t\t\t\t.map((button) => {\n\t\t\t\t\tconst entry = headerActionAsLink(actions.find((action) => action && action.id === button.action))\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(button.id || button.action),\n\t\t\t\t\t\tlabel: this.tr(button.label),\n\t\t\t\t\t\taction: button.action,\n\t\t\t\t\t\tvariant: button.variant === 'primary' ? 'primary' : 'secondary',\n\t\t\t\t\t\ticon: typeof button.icon === 'string' ? button.icon : '',\n\t\t\t\t\t\tlink: entry && typeof entry.href === 'string' && entry.href !== '' ? { href: entry.href, target: entry.target } : null,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The header row in the order it renders. Without the board look:\n\t\t * the declared buttons in the declared order. Under the board look:\n\t\t * the secondary buttons, the buildiq square, then the primary ones,\n\t\t * as the board draws \"Downloaden\" and the square. No declared button\n\t\t * leaves the header as it was, without the square.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @return {Array<object>} The buttons; the square is `{ key: '__buildiq', action: '__buildiq' }`.\n\t\t */\n\t\theaderButtonsRow() {\n\t\t\tconst buttons = this.resolvedHeaderButtons\n\t\t\tif (buttons.length === 0 || !this.isBoard) {\n\t\t\t\treturn buttons\n\t\t\t}\n\t\t\tconst square = { key: '__buildiq', action: '__buildiq', variant: 'secondary', label: '', icon: '' }\n\t\t\treturn [\n\t\t\t\t...buttons.filter((button) => button.variant !== 'primary'),\n\t\t\t\tsquare,\n\t\t\t\t...buttons.filter((button) => button.variant === 'primary'),\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The heading.\n\t\t *\n\t\t * @return {string} The title.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\tconst declared = this.config.title || (this.page && this.page.title)\n\n\t\t\treturn declared ? this.tr(declared) : t('nextcloud-vue', 'Reports')\n\t\t},\n\n\t\t/**\n\t\t * The optional lead paragraph.\n\t\t *\n\t\t * @return {string} The description.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The declared cards, ignoring any that name no route.\n\t\t *\n\t\t * A card with no route is a card that cannot be opened. Rendering it\n\t\t * would put a report on the page that silently does nothing, which is\n\t\t * worse than leaving it off.\n\t\t *\n\t\t * @return {Array<object>} The usable cards.\n\t\t */\n\t\tresolvedCards() {\n\t\t\tconst declared = this.config.cards\n\t\t\tif (!Array.isArray(declared)) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.route && card.label)\n\t\t\t\t.map((card) => ({\n\t\t\t\t\t...card,\n\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Category value/label pairs, offered only for categories that hold a\n\t\t * card. A filter listing empty categories teaches the reader that the\n\t\t * app has reports it does not have.\n\t\t *\n\t\t * @return {Array<object>} The options.\n\t\t */\n\t\tcategoryOptions() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst used = new Set(this.resolvedCards.map((card) => card.category).filter(Boolean))\n\t\t\tconst options = [{ value: 'all', label: this.allLabel }]\n\n\t\t\tObject.keys(declared)\n\t\t\t\t.filter((key) => used.has(key))\n\t\t\t\t.forEach((key) => options.push({ value: key, label: this.tr(declared[key]) }))\n\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * The board look's sections: the cards without a category first, under\n\t\t * no heading, then one section per declared category that holds a\n\t\t * card, in the declared order, headed by its label.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-page-board-look/specs/reports-page/spec.md#requirement-the-reports-page-takes-the-board-look\n\t\t * @return {Array<{key: string, label: string, cards: Array<object>}>} The sections.\n\t\t */\n\t\tboardSections() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst sections = []\n\t\t\tconst loose = this.resolvedCards.filter((card) => !card.category || !Object.hasOwn(declared, card.category))\n\t\t\tif (loose.length > 0) {\n\t\t\t\tsections.push({ key: '__none', label: '', cards: loose })\n\t\t\t}\n\t\t\tfor (const key of Object.keys(declared)) {\n\t\t\t\tconst cards = this.resolvedCards.filter((card) => card.category === key)\n\t\t\t\tif (cards.length > 0) {\n\t\t\t\t\tsections.push({ key, label: this.tr(declared[key]), cards })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn sections\n\t\t},\n\n\t\t/**\n\t\t * The cards the active filter admits.\n\t\t *\n\t\t * @return {Array<object>} The visible cards.\n\t\t */\n\t\tvisibleCards() {\n\t\t\tif (this.activeCategory === 'all') {\n\t\t\t\treturn this.resolvedCards\n\t\t\t}\n\n\t\t\treturn this.resolvedCards.filter((card) => card.category === this.activeCategory)\n\t\t},\n\n\t\t/**\n\t\t * A stable id for the filter control's label association.\n\t\t *\n\t\t * @return {string} The id.\n\t\t */\n\t\tfilterId() {\n\t\t\treturn `cn-reports-category-${(this.page && this.page.id) || 'page'}`\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The \"all categories\" option label.\n\t\t */\n\t\tallLabel() {\n\t\t\treturn this.config.allCategoriesLabel\n\t\t\t\t? this.tr(this.config.allCategoriesLabel)\n\t\t\t\t: t('nextcloud-vue', 'All categories')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The filter's label.\n\t\t */\n\t\tresolvedCategoryLabel() {\n\t\t\treturn this.config.categoryLabel\n\t\t\t\t? this.tr(this.config.categoryLabel)\n\t\t\t\t: t('nextcloud-vue', 'Category')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The empty-state text.\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel\n\t\t\t\t? this.tr(this.config.emptyLabel)\n\t\t\t\t: t('nextcloud-vue', 'No reports in this category.')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest-declared string through the app's own\n\t\t * translate function.\n\t\t *\n\t\t * The manifest is data the renderer walks, not source the extractor\n\t\t * reads, so every string it carries has to be handed to the app's\n\t\t * translator explicitly. A card rendered straight from the manifest\n\t\t * shows its source language whatever locale the reader is in.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translated string, or the input unchanged.\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\treturn this.effectiveTranslate(value)\n\t\t},\n\n\t\t/**\n\t\t * The human label for a card's category, if it has one.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The label, or an empty string.\n\t\t */\n\t\tcategoryName(card) {\n\t\t\tconst declared = this.config.categories || {}\n\n\t\t\treturn this.tr((card.category && declared[card.category]) || '')\n\t\t},\n\n\t\t/**\n\t\t * A real href for the card, so it is a link a reader can middle-click\n\t\t * or open in a new tab rather than a div that only answers to a click.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The href.\n\t\t */\n\t\threfFor(card) {\n\t\t\tif (!this.$router) {\n\t\t\t\treturn '#'\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn this.$router.resolve({ name: card.route }).href\n\t\t\t} catch {\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A header button was clicked: run the `headerActions` entry it names\n\t\t * and emit `header-action`, as an index page's header button does. A\n\t\t * button that is a link is left to the browser.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing\n\t\t * @param {{action: string, link?: object}} button The resolved button.\n\t\t * @return {void}\n\t\t */\n\t\tonHeaderButton(button) {\n\t\t\tif (button.link) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst id = button.action\n\t\t\tconst actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : []\n\t\t\tconst entry = actions.find((action) => action && action.id === id) || null\n\t\t\tconst handler = entry ? entry.handler : undefined\n\t\t\tif (handler === 'none') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof handler === 'function') {\n\t\t\t\thandler({ actionId: id })\n\t\t\t} else if (handler === 'navigate' && entry.route) {\n\t\t\t\tconst params = (entry.params && typeof entry.params === 'object') ? entry.params : null\n\t\t\t\tif (this.$router && typeof this.$router.push === 'function') {\n\t\t\t\t\tthis.$router.push(params ? { name: entry.route, params } : { name: entry.route })\n\t\t\t\t}\n\t\t\t} else if (entry && entry.type === 'navigate' && typeof entry.target === 'string' && entry.target !== '') {\n\t\t\t\twindow.open(entry.target, '_blank', 'noopener,noreferrer')\n\t\t\t} else if (typeof handler === 'string' && handler !== 'emit') {\n\t\t\t\tconst resolved = resolveRegisteredHandler(handler, this.cnRegistry, this.cnCustomComponents)\n\t\t\t\tif (typeof resolved === 'function') {\n\t\t\t\t\tresolved({ actionId: id })\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * A header button was clicked (`headerButtons`), after its\n\t\t\t * `headerActions` entry ran; not for `handler: \"none\"`.\n\t\t\t *\n\t\t\t * @event header-action\n\t\t\t * @property {{action: string, id: string}} payload The id of the `headerActions` entry the button names, as `action` and `id`.\n\t\t\t */\n\t\t\tthis.$emit('header-action', { action: id, id })\n\t\t},\n\n\t\t/**\n\t\t * Navigate by route NAME on a plain click; a modified or middle click\n\t\t * is left to the browser so it can open the card's href elsewhere.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t */\n\t\topen(card, event) {\n\t\t\tfollowLinkClick(event, { name: card.route }, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-reports-page {\n\tpadding: 16px;\n}\n\n/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n positioned at the left edge of .app-content) plus 12px breathing room, as\n CnPageHeader does. Only the header shifts; the body keeps the full width. */\n.cn-reports-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-reports-page__title {\n\tmargin: 0 0 4px;\n}\n\n.cn-reports-page__heading-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-start;\n\tjustify-content: space-between;\n\tgap: 12px 24px;\n}\n\n.cn-reports-page__heading-row .cn-reports-page__title {\n\tflex: 1 1 320px;\n\tmin-width: 0;\n}\n\n.cn-reports-page__header-buttons {\n\tdisplay: flex;\n\tflex: none;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 10px;\n}\n\n.cn-reports-page__description {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__filter {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 16px;\n}\n\n.cn-reports-page__filter-select {\n\tmin-width: 220px;\n}\n\n.cn-reports-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 12px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-reports-page__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\theight: 100%;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-reports-page__card:hover,\n.cn-reports-page__card:focus {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-reports-page__card-title {\n\tfont-weight: bold;\n}\n\n.cn-reports-page__card-description,\n.cn-reports-page__card-category {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-reports-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__widgets {\n\tmargin-top: 20px;\n}\n</style>\n"],"names":["CnBuildiqEditButton","CnIcon","t"],"mappings":"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"}
|
|
@@ -99,6 +99,8 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
99
99
|
: (openBlock(), createBlock(_component_NcButton, {
|
|
100
100
|
key: 1,
|
|
101
101
|
variant: button.variant,
|
|
102
|
+
href: button.link ? button.link.href : undefined,
|
|
103
|
+
target: button.link ? button.link.target : undefined,
|
|
102
104
|
"data-testid": `cn-reports-header-button-${button.key}`,
|
|
103
105
|
onClick: $event => ($options.onHeaderButton(button))
|
|
104
106
|
}, createSlots({
|
|
@@ -119,7 +121,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
119
121
|
key: "0"
|
|
120
122
|
}
|
|
121
123
|
: undefined
|
|
122
|
-
]), 1032 /* PROPS, DYNAMIC_SLOTS */, ["variant", "data-testid", "onClick"]))
|
|
124
|
+
]), 1032 /* PROPS, DYNAMIC_SLOTS */, ["variant", "href", "target", "data-testid", "onClick"]))
|
|
123
125
|
], 64 /* STABLE_FRAGMENT */))
|
|
124
126
|
}), 128 /* KEYED_FRAGMENT */))
|
|
125
127
|
])
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnReportsPage.vue3.js","sources":["../../../../src/components/CnReportsPage/CnReportsPage.vue?vue&type=template&id=327b60f8&lang.js"],"sourcesContent":["<!--\n CnReportsPage — Manifest-driven Reports surface.\n\n v2 manifest page primitive for `type: \"reports\"`. One page holding every\n report an app offers, as cards, instead of a Reports SUBMENU with one entry\n per report.\n\n WHY THIS IS A PAGE AND NOT A MENU BRANCH. A submenu grows one entry per\n report and never shrinks. shillinq reached 96 report types; as menu children\n that is a scroll, not a navigation. Cards carry what a menu item cannot — a\n description, a category, a count — and a category filter turns 96 into a\n short list. shillinq proved the shape in `ReportingComplianceOverview`; this\n is that shape with the app-specific parts taken out, so every app declares it\n rather than copying 600 lines of Vue.\n\n Declare it as:\n\n { \"id\": \"Reports\", \"route\": \"/reports\", \"type\": \"reports\",\n \"config\": {\n \"categories\": { \"operational\": \"Operational\" },\n \"cards\": [\n { \"id\": \"Throughput\", \"label\": \"Processing time\",\n \"description\": \"How long cases take, by type.\",\n \"icon\": \"ChartLine\", \"category\": \"operational\",\n \"route\": \"Doorlooptijd\" }\n ]\n } }\n\n A card names a ROUTE by name, never a path: a path is editable per app and a\n card pointing at a stale one is a dead end that still looks like a report.\n\n Spec: hydra openspec/architecture/adr-112-reports-are-one-page.md (ConductionNL/hydra#640).\n-->\n<template>\n\t<div class=\"cn-reports-page\" :class=\"{ 'cn-reports-page--board': isBoard }\">\n\t\t<div class=\"cn-reports-page__header\">\n\t\t\t<!-- The header buttons (`headerButtons`) beside the title, as the\n\t\t\t board's \"Downloaden\" and the buildiq square (board DcRapportages).\n\t\t\t Without them, and outside the board look, the title stands alone\n\t\t\t as before. -->\n\t\t\t<div v-if=\"headerButtonsRow.length > 0\" class=\"cn-reports-page__heading-row\">\n\t\t\t\t<h2 class=\"cn-reports-page__title\">\n\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t</h2>\n\t\t\t\t<div class=\"cn-reports-page__header-buttons\" data-testid=\"cn-reports-header-buttons\">\n\t\t\t\t\t<template v-for=\"button in headerButtonsRow\" :key=\"button.key\">\n\t\t\t\t\t\t<CnBuildiqEditButton v-if=\"button.action === '__buildiq'\" />\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t:variant=\"button.variant\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-reports-header-button-${button.key}`\"\n\t\t\t\t\t\t\t@click=\"onHeaderButton(button)\">\n\t\t\t\t\t\t\t<template v-if=\"button.icon\" #icon>\n\t\t\t\t\t\t\t\t<CnIcon :name=\"button.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ button.label }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</template>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<h2 v-else class=\"cn-reports-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-reports-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<!-- The board look (board DcRapportages): one section per category,\n\t\t headed by its label, instead of the category filter; each card\n\t\t draws its icon in a tile. -->\n\t\t<template v-if=\"isBoard\">\n\t\t\t<p v-if=\"resolvedCards.length === 0\"\n\t\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t\t{{ resolvedEmptyLabel }}\n\t\t\t</p>\n\t\t\t<section v-for=\"section in boardSections\"\n\t\t\t\t:key=\"section.key\"\n\t\t\t\tclass=\"cn-reports-page__section\"\n\t\t\t\tdata-testid=\"cn-reports-section\">\n\t\t\t\t<h3 v-if=\"section.label\" class=\"cn-reports-page__section-title\">\n\t\t\t\t\t{{ section.label }}\n\t\t\t\t</h3>\n\t\t\t\t<ul class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t\t\t<li v-for=\"card in section.cards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t\t\t<span v-if=\"card.icon\" class=\"cn-reports-page__card-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t<CnIcon :name=\"card.icon\" :size=\"22\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span class=\"cn-reports-page__card-text\">\n\t\t\t\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\t\t</template>\n\n\t\t<div v-if=\"!isBoard && categoryOptions.length > 1\" class=\"cn-reports-page__filter\">\n\t\t\t<label class=\"cn-reports-page__filter-label\" :for=\"filterId\">\n\t\t\t\t{{ resolvedCategoryLabel }}\n\t\t\t</label>\n\t\t\t<select :id=\"filterId\"\n\t\t\t\tv-model=\"activeCategory\"\n\t\t\t\tclass=\"cn-reports-page__filter-select\"\n\t\t\t\tdata-testid=\"cn-reports-category\">\n\t\t\t\t<option v-for=\"option in categoryOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:value=\"option.value\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<p v-if=\"!isBoard && visibleCards.length === 0\"\n\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<ul v-else-if=\"!isBoard\" class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t<li v-for=\"card in visibleCards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"categoryName(card)\" class=\"cn-reports-page__card-category\">\n\t\t\t\t\t\t{{ categoryName(card) }}\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<!-- `widgets` + `layout`: a grid of dashboard widgets under the cards,\n\t\t such as a highlighted report with stat tiles and a chart. -->\n\t\t<div v-if=\"sectionWidgets.length > 0\" class=\"cn-reports-page__widgets\" data-testid=\"cn-reports-widgets\">\n\t\t\t<CnDashboardPage\n\t\t\t\t:widgets=\"sectionWidgets\"\n\t\t\t\t:layout=\"sectionLayout\"\n\t\t\t\t:showHeader=\"false\" />\n\t\t</div>\n\t</div>\n</template>\n\n<script>\n/**\n * The Reports page: one surface listing an app's reports as cards.\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton } from '@nextcloud/vue'\nimport { defineAsyncComponent } from 'vue'\nimport CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport { useLook } from '../../composables/useLook.js'\nimport { resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'\nimport { followLinkClick } from '../../utils/linkNavigation.js'\n\nexport default {\n\tname: 'CnReportsPage',\n\n\tcomponents: {\n\t\t// Loaded only for a page that declares widgets: a cards-only reports\n\t\t// page does not pull the dashboard in.\n\t\tCnDashboardPage: defineAsyncComponent(() => import('../CnDashboardPage/CnDashboardPage.vue').then((m) => m.default)),\n\t\tCnBuildiqEditButton,\n\t\tCnIcon,\n\t\tNcButton,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * The v2 registry and the legacy customComponents map, provided by\n\t\t * CnAppRoot: a header action's named handler resolves against them.\n\t\t */\n\t\tcnRegistry: { default: () => ({}) },\n\t\tcnCustomComponents: { default: () => ({}) },\n\t\t/**\n\t\t * The consuming app's translate function, provided by CnAppRoot.\n\t\t * Defaults to identity so the page still renders outside one.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"reports\"` page arrived here with the\n\t\t * default `{}` — `config` resolved to `{}`, `cards` to `[]`, and every\n\t\t * reports page in the fleet rendered its empty state while its manifest\n\t\t * declared cards. Kept because a host that renders this component\n\t\t * directly can still hand it the whole page; the flattened props below\n\t\t * are what the renderer actually supplies, and they win.\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The report cards, as CnPageRenderer flattens them out of `config`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Category key/label pairs, flattened out of `config`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The heading, lifted from the page by the renderer.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the \"all categories\" filter option.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tallCategoriesLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the category filter control.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcategoryLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Text shown when the active filter admits no card.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Dashboard widgets drawn under the report cards (`config.widgets`),\n\t\t * placed by `layout`, as on a `type: \"dashboard\"` page.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\twidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Grid placement of `widgets` (`config.layout`): `{ id, widgetId,\n\t\t * gridX, gridY, gridWidth, gridHeight }` per widget. Without it each\n\t\t * widget takes a full row in the order declared.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tlayout: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Buttons beside the title (`config.headerButtons`), as the board's\n\t\t * \"Downloaden\". Each is `{ label, action, variant?, icon?, id? }`:\n\t\t * `action` is the id of a `headerActions` entry, which says what the\n\t\t * click does. Under the board look the buildiq square sits between the\n\t\t * secondary buttons and the primary one. Empty (the default) changes\n\t\t * nothing.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @type {Array<{label: string, action: string, variant?: ('primary'|'secondary'), icon?: string, id?: string}>|null}\n\t\t */\n\t\theaderButtons: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The page's header actions (`config.headerActions`), in the shape of\n\t\t * an index page's: a header button names one by id. `handler` is a\n\t\t * function, `navigate` (with `route`, and `params`), `emit`, `none`,\n\t\t * or a registry name; `type: \"navigate\"` with a `target` URL opens\n\t\t * that URL in a new tab. Every click also emits `header-action`\n\t\t * `{ action, id }`, except for `handler: \"none\"`.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\theaderActions: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The look to draw in: `board` or `nextcloud`. Absent: the look\n\t\t * CnAppRoot provides (`cnLook`), else `nextcloud`.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tlook: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['header-action'],\n\n\tsetup(props) {\n\t\tconst { isBoard } = useLook(props)\n\t\treturn { isBoard }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveCategory: 'all',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The translate function actually in force.\n\t\t *\n\t\t * @return {(key: string) => string} The translator.\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The declared config, always an object, from whichever direction it\n\t\t * arrived.\n\t\t *\n\t\t * TWO CALLERS, TWO SHAPES. CnPageRenderer flattens `page.config.*` into\n\t\t * individual props and passes NO `page`, so under the renderer the keys\n\t\t * arrive as props. A host that mounts this component itself hands it the\n\t\t * whole `page`. Reading only `page.config` is what made every dispatched\n\t\t * reports page render empty.\n\t\t *\n\t\t * Props win where both are present: they are the nearer declaration, and\n\t\t * the renderer has already resolved route sentinels in them.\n\t\t *\n\t\t * @return {object} The config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of [\n\t\t\t\t'cards',\n\t\t\t\t'categories',\n\t\t\t\t'description',\n\t\t\t\t'title',\n\t\t\t\t'allCategoriesLabel',\n\t\t\t\t'categoryLabel',\n\t\t\t\t'emptyLabel',\n\t\t\t\t'widgets',\n\t\t\t\t'layout',\n\t\t\t\t'headerButtons',\n\t\t\t\t'headerActions',\n\t\t\t]) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The widgets under the cards.\n\t\t *\n\t\t * @return {Array<object>} The declared widgets, or none.\n\t\t * @spec openspec/changes/screens-reports-page-widgets/specs/reports-page/spec.md\n\t\t */\n\t\tsectionWidgets() {\n\t\t\tconst widgets = this.config.widgets\n\t\t\treturn Array.isArray(widgets) ? widgets.filter((w) => w && typeof w === 'object' && w.id) : []\n\t\t},\n\n\t\t/**\n\t\t * Where the widgets go: the declared `layout`, else one full row per\n\t\t * widget in the order declared.\n\t\t *\n\t\t * @return {Array<object>} The placements.\n\t\t * @spec openspec/changes/screens-reports-page-widgets/specs/reports-page/spec.md\n\t\t */\n\t\tsectionLayout() {\n\t\t\tif (Array.isArray(this.config.layout) && this.config.layout.length > 0) {\n\t\t\t\treturn this.config.layout\n\t\t\t}\n\t\t\tlet y = 0\n\t\t\treturn this.sectionWidgets.map((widget, index) => {\n\t\t\t\tconst placement = { id: String(index + 1), widgetId: widget.id, gridX: 0, gridY: y, gridWidth: 12, gridHeight: 4 }\n\t\t\t\ty += 4\n\t\t\t\treturn placement\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The declared header buttons that have a label and an action,\n\t\t * translated, each with a stable key and a variant.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @return {Array<{key: string, label: string, action: string, variant: string, icon: string}>} The buttons.\n\t\t */\n\t\tresolvedHeaderButtons() {\n\t\t\tconst declared = Array.isArray(this.config.headerButtons) ? this.config.headerButtons : []\n\t\t\treturn declared\n\t\t\t\t.filter((button) => button && typeof button === 'object'\n\t\t\t\t\t&& typeof button.action === 'string' && button.action !== ''\n\t\t\t\t\t&& typeof button.label === 'string' && button.label !== '')\n\t\t\t\t.map((button) => ({\n\t\t\t\t\tkey: String(button.id || button.action),\n\t\t\t\t\tlabel: this.tr(button.label),\n\t\t\t\t\taction: button.action,\n\t\t\t\t\tvariant: button.variant === 'primary' ? 'primary' : 'secondary',\n\t\t\t\t\ticon: typeof button.icon === 'string' ? button.icon : '',\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The header row in the order it renders. Without the board look:\n\t\t * the declared buttons in the declared order. Under the board look:\n\t\t * the secondary buttons, the buildiq square, then the primary ones,\n\t\t * as the board draws \"Downloaden\" and the square. No declared button\n\t\t * leaves the header as it was, without the square.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @return {Array<object>} The buttons; the square is `{ key: '__buildiq', action: '__buildiq' }`.\n\t\t */\n\t\theaderButtonsRow() {\n\t\t\tconst buttons = this.resolvedHeaderButtons\n\t\t\tif (buttons.length === 0 || !this.isBoard) {\n\t\t\t\treturn buttons\n\t\t\t}\n\t\t\tconst square = { key: '__buildiq', action: '__buildiq', variant: 'secondary', label: '', icon: '' }\n\t\t\treturn [\n\t\t\t\t...buttons.filter((button) => button.variant !== 'primary'),\n\t\t\t\tsquare,\n\t\t\t\t...buttons.filter((button) => button.variant === 'primary'),\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The heading.\n\t\t *\n\t\t * @return {string} The title.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\tconst declared = this.config.title || (this.page && this.page.title)\n\n\t\t\treturn declared ? this.tr(declared) : t('nextcloud-vue', 'Reports')\n\t\t},\n\n\t\t/**\n\t\t * The optional lead paragraph.\n\t\t *\n\t\t * @return {string} The description.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The declared cards, ignoring any that name no route.\n\t\t *\n\t\t * A card with no route is a card that cannot be opened. Rendering it\n\t\t * would put a report on the page that silently does nothing, which is\n\t\t * worse than leaving it off.\n\t\t *\n\t\t * @return {Array<object>} The usable cards.\n\t\t */\n\t\tresolvedCards() {\n\t\t\tconst declared = this.config.cards\n\t\t\tif (!Array.isArray(declared)) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.route && card.label)\n\t\t\t\t.map((card) => ({\n\t\t\t\t\t...card,\n\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Category value/label pairs, offered only for categories that hold a\n\t\t * card. A filter listing empty categories teaches the reader that the\n\t\t * app has reports it does not have.\n\t\t *\n\t\t * @return {Array<object>} The options.\n\t\t */\n\t\tcategoryOptions() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst used = new Set(this.resolvedCards.map((card) => card.category).filter(Boolean))\n\t\t\tconst options = [{ value: 'all', label: this.allLabel }]\n\n\t\t\tObject.keys(declared)\n\t\t\t\t.filter((key) => used.has(key))\n\t\t\t\t.forEach((key) => options.push({ value: key, label: this.tr(declared[key]) }))\n\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * The board look's sections: the cards without a category first, under\n\t\t * no heading, then one section per declared category that holds a\n\t\t * card, in the declared order, headed by its label.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-page-board-look/specs/reports-page/spec.md#requirement-the-reports-page-takes-the-board-look\n\t\t * @return {Array<{key: string, label: string, cards: Array<object>}>} The sections.\n\t\t */\n\t\tboardSections() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst sections = []\n\t\t\tconst loose = this.resolvedCards.filter((card) => !card.category || !Object.hasOwn(declared, card.category))\n\t\t\tif (loose.length > 0) {\n\t\t\t\tsections.push({ key: '__none', label: '', cards: loose })\n\t\t\t}\n\t\t\tfor (const key of Object.keys(declared)) {\n\t\t\t\tconst cards = this.resolvedCards.filter((card) => card.category === key)\n\t\t\t\tif (cards.length > 0) {\n\t\t\t\t\tsections.push({ key, label: this.tr(declared[key]), cards })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn sections\n\t\t},\n\n\t\t/**\n\t\t * The cards the active filter admits.\n\t\t *\n\t\t * @return {Array<object>} The visible cards.\n\t\t */\n\t\tvisibleCards() {\n\t\t\tif (this.activeCategory === 'all') {\n\t\t\t\treturn this.resolvedCards\n\t\t\t}\n\n\t\t\treturn this.resolvedCards.filter((card) => card.category === this.activeCategory)\n\t\t},\n\n\t\t/**\n\t\t * A stable id for the filter control's label association.\n\t\t *\n\t\t * @return {string} The id.\n\t\t */\n\t\tfilterId() {\n\t\t\treturn `cn-reports-category-${(this.page && this.page.id) || 'page'}`\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The \"all categories\" option label.\n\t\t */\n\t\tallLabel() {\n\t\t\treturn this.config.allCategoriesLabel\n\t\t\t\t? this.tr(this.config.allCategoriesLabel)\n\t\t\t\t: t('nextcloud-vue', 'All categories')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The filter's label.\n\t\t */\n\t\tresolvedCategoryLabel() {\n\t\t\treturn this.config.categoryLabel\n\t\t\t\t? this.tr(this.config.categoryLabel)\n\t\t\t\t: t('nextcloud-vue', 'Category')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The empty-state text.\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel\n\t\t\t\t? this.tr(this.config.emptyLabel)\n\t\t\t\t: t('nextcloud-vue', 'No reports in this category.')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest-declared string through the app's own\n\t\t * translate function.\n\t\t *\n\t\t * The manifest is data the renderer walks, not source the extractor\n\t\t * reads, so every string it carries has to be handed to the app's\n\t\t * translator explicitly. A card rendered straight from the manifest\n\t\t * shows its source language whatever locale the reader is in.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translated string, or the input unchanged.\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\treturn this.effectiveTranslate(value)\n\t\t},\n\n\t\t/**\n\t\t * The human label for a card's category, if it has one.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The label, or an empty string.\n\t\t */\n\t\tcategoryName(card) {\n\t\t\tconst declared = this.config.categories || {}\n\n\t\t\treturn this.tr((card.category && declared[card.category]) || '')\n\t\t},\n\n\t\t/**\n\t\t * A real href for the card, so it is a link a reader can middle-click\n\t\t * or open in a new tab rather than a div that only answers to a click.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The href.\n\t\t */\n\t\threfFor(card) {\n\t\t\tif (!this.$router) {\n\t\t\t\treturn '#'\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn this.$router.resolve({ name: card.route }).href\n\t\t\t} catch {\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A header button was clicked: run the `headerActions` entry it names\n\t\t * and emit `header-action`, as an index page's header button does.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @param {{action: string}} button The resolved button.\n\t\t * @return {void}\n\t\t */\n\t\tonHeaderButton(button) {\n\t\t\tconst id = button.action\n\t\t\tconst actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : []\n\t\t\tconst entry = actions.find((action) => action && action.id === id) || null\n\t\t\tconst handler = entry ? entry.handler : undefined\n\t\t\tif (handler === 'none') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof handler === 'function') {\n\t\t\t\thandler({ actionId: id })\n\t\t\t} else if (handler === 'navigate' && entry.route) {\n\t\t\t\tconst params = (entry.params && typeof entry.params === 'object') ? entry.params : null\n\t\t\t\tif (this.$router && typeof this.$router.push === 'function') {\n\t\t\t\t\tthis.$router.push(params ? { name: entry.route, params } : { name: entry.route })\n\t\t\t\t}\n\t\t\t} else if (entry && entry.type === 'navigate' && typeof entry.target === 'string' && entry.target !== '') {\n\t\t\t\twindow.open(entry.target, '_blank', 'noopener,noreferrer')\n\t\t\t} else if (typeof handler === 'string' && handler !== 'emit') {\n\t\t\t\tconst resolved = resolveRegisteredHandler(handler, this.cnRegistry, this.cnCustomComponents)\n\t\t\t\tif (typeof resolved === 'function') {\n\t\t\t\t\tresolved({ actionId: id })\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * A header button was clicked (`headerButtons`), after its\n\t\t\t * `headerActions` entry ran; not for `handler: \"none\"`.\n\t\t\t *\n\t\t\t * @event header-action\n\t\t\t * @property {{action: string, id: string}} payload The id of the `headerActions` entry the button names, as `action` and `id`.\n\t\t\t */\n\t\t\tthis.$emit('header-action', { action: id, id })\n\t\t},\n\n\t\t/**\n\t\t * Navigate by route NAME on a plain click; a modified or middle click\n\t\t * is left to the browser so it can open the card's href elsewhere.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t */\n\t\topen(card, event) {\n\t\t\tfollowLinkClick(event, { name: card.route }, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-reports-page {\n\tpadding: 16px;\n}\n\n/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n positioned at the left edge of .app-content) plus 12px breathing room, as\n CnPageHeader does. Only the header shifts; the body keeps the full width. */\n.cn-reports-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-reports-page__title {\n\tmargin: 0 0 4px;\n}\n\n.cn-reports-page__heading-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-start;\n\tjustify-content: space-between;\n\tgap: 12px 24px;\n}\n\n.cn-reports-page__heading-row .cn-reports-page__title {\n\tflex: 1 1 320px;\n\tmin-width: 0;\n}\n\n.cn-reports-page__header-buttons {\n\tdisplay: flex;\n\tflex: none;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 10px;\n}\n\n.cn-reports-page__description {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__filter {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 16px;\n}\n\n.cn-reports-page__filter-select {\n\tmin-width: 220px;\n}\n\n.cn-reports-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 12px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-reports-page__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\theight: 100%;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-reports-page__card:hover,\n.cn-reports-page__card:focus {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-reports-page__card-title {\n\tfont-weight: bold;\n}\n\n.cn-reports-page__card-description,\n.cn-reports-page__card-category {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-reports-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__widgets {\n\tmargin-top: 20px;\n}\n</style>\n"],"names":["_createElementVNode","_toDisplayString","_createElementBlock","_Fragment","_renderList","_createBlock","_createVNode","_openBlock"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnReportsPage.vue3.js","sources":["../../../../src/components/CnReportsPage/CnReportsPage.vue?vue&type=template&id=6feca28f&lang.js"],"sourcesContent":["<!--\n CnReportsPage — Manifest-driven Reports surface.\n\n v2 manifest page primitive for `type: \"reports\"`. One page holding every\n report an app offers, as cards, instead of a Reports SUBMENU with one entry\n per report.\n\n WHY THIS IS A PAGE AND NOT A MENU BRANCH. A submenu grows one entry per\n report and never shrinks. shillinq reached 96 report types; as menu children\n that is a scroll, not a navigation. Cards carry what a menu item cannot — a\n description, a category, a count — and a category filter turns 96 into a\n short list. shillinq proved the shape in `ReportingComplianceOverview`; this\n is that shape with the app-specific parts taken out, so every app declares it\n rather than copying 600 lines of Vue.\n\n Declare it as:\n\n { \"id\": \"Reports\", \"route\": \"/reports\", \"type\": \"reports\",\n \"config\": {\n \"categories\": { \"operational\": \"Operational\" },\n \"cards\": [\n { \"id\": \"Throughput\", \"label\": \"Processing time\",\n \"description\": \"How long cases take, by type.\",\n \"icon\": \"ChartLine\", \"category\": \"operational\",\n \"route\": \"Doorlooptijd\" }\n ]\n } }\n\n A card names a ROUTE by name, never a path: a path is editable per app and a\n card pointing at a stale one is a dead end that still looks like a report.\n\n Spec: hydra openspec/architecture/adr-112-reports-are-one-page.md (ConductionNL/hydra#640).\n-->\n<template>\n\t<div class=\"cn-reports-page\" :class=\"{ 'cn-reports-page--board': isBoard }\">\n\t\t<div class=\"cn-reports-page__header\">\n\t\t\t<!-- The header buttons (`headerButtons`) beside the title, as the\n\t\t\t board's \"Downloaden\" and the buildiq square (board DcRapportages).\n\t\t\t Without them, and outside the board look, the title stands alone\n\t\t\t as before. -->\n\t\t\t<div v-if=\"headerButtonsRow.length > 0\" class=\"cn-reports-page__heading-row\">\n\t\t\t\t<h2 class=\"cn-reports-page__title\">\n\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t</h2>\n\t\t\t\t<div class=\"cn-reports-page__header-buttons\" data-testid=\"cn-reports-header-buttons\">\n\t\t\t\t\t<template v-for=\"button in headerButtonsRow\" :key=\"button.key\">\n\t\t\t\t\t\t<CnBuildiqEditButton v-if=\"button.action === '__buildiq'\" />\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t:variant=\"button.variant\"\n\t\t\t\t\t\t\t:href=\"button.link ? button.link.href : undefined\"\n\t\t\t\t\t\t\t:target=\"button.link ? button.link.target : undefined\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-reports-header-button-${button.key}`\"\n\t\t\t\t\t\t\t@click=\"onHeaderButton(button)\">\n\t\t\t\t\t\t\t<template v-if=\"button.icon\" #icon>\n\t\t\t\t\t\t\t\t<CnIcon :name=\"button.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ button.label }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</template>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<h2 v-else class=\"cn-reports-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-reports-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<!-- The board look (board DcRapportages): one section per category,\n\t\t headed by its label, instead of the category filter; each card\n\t\t draws its icon in a tile. -->\n\t\t<template v-if=\"isBoard\">\n\t\t\t<p v-if=\"resolvedCards.length === 0\"\n\t\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t\t{{ resolvedEmptyLabel }}\n\t\t\t</p>\n\t\t\t<section v-for=\"section in boardSections\"\n\t\t\t\t:key=\"section.key\"\n\t\t\t\tclass=\"cn-reports-page__section\"\n\t\t\t\tdata-testid=\"cn-reports-section\">\n\t\t\t\t<h3 v-if=\"section.label\" class=\"cn-reports-page__section-title\">\n\t\t\t\t\t{{ section.label }}\n\t\t\t\t</h3>\n\t\t\t\t<ul class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t\t\t<li v-for=\"card in section.cards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t\t\t<span v-if=\"card.icon\" class=\"cn-reports-page__card-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t<CnIcon :name=\"card.icon\" :size=\"22\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span class=\"cn-reports-page__card-text\">\n\t\t\t\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\t\t</template>\n\n\t\t<div v-if=\"!isBoard && categoryOptions.length > 1\" class=\"cn-reports-page__filter\">\n\t\t\t<label class=\"cn-reports-page__filter-label\" :for=\"filterId\">\n\t\t\t\t{{ resolvedCategoryLabel }}\n\t\t\t</label>\n\t\t\t<select :id=\"filterId\"\n\t\t\t\tv-model=\"activeCategory\"\n\t\t\t\tclass=\"cn-reports-page__filter-select\"\n\t\t\t\tdata-testid=\"cn-reports-category\">\n\t\t\t\t<option v-for=\"option in categoryOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:value=\"option.value\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<p v-if=\"!isBoard && visibleCards.length === 0\"\n\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<ul v-else-if=\"!isBoard\" class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t<li v-for=\"card in visibleCards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"categoryName(card)\" class=\"cn-reports-page__card-category\">\n\t\t\t\t\t\t{{ categoryName(card) }}\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<!-- `widgets` + `layout`: a grid of dashboard widgets under the cards,\n\t\t such as a highlighted report with stat tiles and a chart. -->\n\t\t<div v-if=\"sectionWidgets.length > 0\" class=\"cn-reports-page__widgets\" data-testid=\"cn-reports-widgets\">\n\t\t\t<CnDashboardPage\n\t\t\t\t:widgets=\"sectionWidgets\"\n\t\t\t\t:layout=\"sectionLayout\"\n\t\t\t\t:showHeader=\"false\" />\n\t\t</div>\n\t</div>\n</template>\n\n<script>\n/**\n * The Reports page: one surface listing an app's reports as cards.\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton } from '@nextcloud/vue'\nimport { defineAsyncComponent } from 'vue'\nimport CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport { useLook } from '../../composables/useLook.js'\nimport { headerActionAsLink } from '../../utils/actionLink.js'\nimport { resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'\nimport { followLinkClick } from '../../utils/linkNavigation.js'\n\nexport default {\n\tname: 'CnReportsPage',\n\n\tcomponents: {\n\t\t// Loaded only for a page that declares widgets: a cards-only reports\n\t\t// page does not pull the dashboard in.\n\t\tCnDashboardPage: defineAsyncComponent(() => import('../CnDashboardPage/CnDashboardPage.vue').then((m) => m.default)),\n\t\tCnBuildiqEditButton,\n\t\tCnIcon,\n\t\tNcButton,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * The v2 registry and the legacy customComponents map, provided by\n\t\t * CnAppRoot: a header action's named handler resolves against them.\n\t\t */\n\t\tcnRegistry: { default: () => ({}) },\n\t\tcnCustomComponents: { default: () => ({}) },\n\t\t/**\n\t\t * The consuming app's translate function, provided by CnAppRoot.\n\t\t * Defaults to identity so the page still renders outside one.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"reports\"` page arrived here with the\n\t\t * default `{}` — `config` resolved to `{}`, `cards` to `[]`, and every\n\t\t * reports page in the fleet rendered its empty state while its manifest\n\t\t * declared cards. Kept because a host that renders this component\n\t\t * directly can still hand it the whole page; the flattened props below\n\t\t * are what the renderer actually supplies, and they win.\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The report cards, as CnPageRenderer flattens them out of `config`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Category key/label pairs, flattened out of `config`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The heading, lifted from the page by the renderer.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the \"all categories\" filter option.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tallCategoriesLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the category filter control.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcategoryLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Text shown when the active filter admits no card.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Dashboard widgets drawn under the report cards (`config.widgets`),\n\t\t * placed by `layout`, as on a `type: \"dashboard\"` page.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\twidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Grid placement of `widgets` (`config.layout`): `{ id, widgetId,\n\t\t * gridX, gridY, gridWidth, gridHeight }` per widget. Without it each\n\t\t * widget takes a full row in the order declared.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tlayout: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Buttons beside the title (`config.headerButtons`), as the board's\n\t\t * \"Downloaden\". Each is `{ label, action, variant?, icon?, id? }`:\n\t\t * `action` is the id of a `headerActions` entry, which says what the\n\t\t * click does. Under the board look the buildiq square sits between the\n\t\t * secondary buttons and the primary one. Empty (the default) changes\n\t\t * nothing.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @type {Array<{label: string, action: string, variant?: ('primary'|'secondary'), icon?: string, id?: string}>|null}\n\t\t */\n\t\theaderButtons: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The page's header actions (`config.headerActions`), in the shape of\n\t\t * an index page's: a header button names one by id. `handler` is a\n\t\t * function, `navigate` (with `route`, and `params`), `emit`, `none`,\n\t\t * or a registry name; `type: \"navigate\"` with a `target` URL opens\n\t\t * that URL in a new tab. Every click also emits `header-action`\n\t\t * `{ action, id }`, except for `handler: \"none\"`. An entry with `href`\n\t\t * makes its button a real link that runs and emits nothing: an\n\t\t * absolute `http(s)`, `mailto:`, `tel:` or `//host/...` URL as written,\n\t\t * an instance path (leading `/`) through `generateUrl()`, in a new tab with\n\t\t * `target: '_blank'`.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\theaderActions: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The look to draw in: `board` or `nextcloud`. Absent: the look\n\t\t * CnAppRoot provides (`cnLook`), else `nextcloud`.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tlook: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['header-action'],\n\n\tsetup(props) {\n\t\tconst { isBoard } = useLook(props)\n\t\treturn { isBoard }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveCategory: 'all',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The translate function actually in force.\n\t\t *\n\t\t * @return {(key: string) => string} The translator.\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The declared config, always an object, from whichever direction it\n\t\t * arrived.\n\t\t *\n\t\t * TWO CALLERS, TWO SHAPES. CnPageRenderer flattens `page.config.*` into\n\t\t * individual props and passes NO `page`, so under the renderer the keys\n\t\t * arrive as props. A host that mounts this component itself hands it the\n\t\t * whole `page`. Reading only `page.config` is what made every dispatched\n\t\t * reports page render empty.\n\t\t *\n\t\t * Props win where both are present: they are the nearer declaration, and\n\t\t * the renderer has already resolved route sentinels in them.\n\t\t *\n\t\t * @return {object} The config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of [\n\t\t\t\t'cards',\n\t\t\t\t'categories',\n\t\t\t\t'description',\n\t\t\t\t'title',\n\t\t\t\t'allCategoriesLabel',\n\t\t\t\t'categoryLabel',\n\t\t\t\t'emptyLabel',\n\t\t\t\t'widgets',\n\t\t\t\t'layout',\n\t\t\t\t'headerButtons',\n\t\t\t\t'headerActions',\n\t\t\t]) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The widgets under the cards.\n\t\t *\n\t\t * @return {Array<object>} The declared widgets, or none.\n\t\t * @spec openspec/changes/screens-reports-page-widgets/specs/reports-page/spec.md\n\t\t */\n\t\tsectionWidgets() {\n\t\t\tconst widgets = this.config.widgets\n\t\t\treturn Array.isArray(widgets) ? widgets.filter((w) => w && typeof w === 'object' && w.id) : []\n\t\t},\n\n\t\t/**\n\t\t * Where the widgets go: the declared `layout`, else one full row per\n\t\t * widget in the order declared.\n\t\t *\n\t\t * @return {Array<object>} The placements.\n\t\t * @spec openspec/changes/screens-reports-page-widgets/specs/reports-page/spec.md\n\t\t */\n\t\tsectionLayout() {\n\t\t\tif (Array.isArray(this.config.layout) && this.config.layout.length > 0) {\n\t\t\t\treturn this.config.layout\n\t\t\t}\n\t\t\tlet y = 0\n\t\t\treturn this.sectionWidgets.map((widget, index) => {\n\t\t\t\tconst placement = { id: String(index + 1), widgetId: widget.id, gridX: 0, gridY: y, gridWidth: 12, gridHeight: 4 }\n\t\t\t\ty += 4\n\t\t\t\treturn placement\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The declared header buttons that have a label and an action,\n\t\t * translated, each with a stable key and a variant. A button naming a\n\t\t * header action with `href` carries that `link`.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing\n\t\t * @return {Array<{key: string, label: string, action: string, variant: string, icon: string, link: ({href: string, target: string}|null)}>} The buttons.\n\t\t */\n\t\tresolvedHeaderButtons() {\n\t\t\tconst declared = Array.isArray(this.config.headerButtons) ? this.config.headerButtons : []\n\t\t\tconst actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : []\n\t\t\treturn declared\n\t\t\t\t.filter((button) => button && typeof button === 'object'\n\t\t\t\t\t&& typeof button.action === 'string' && button.action !== ''\n\t\t\t\t\t&& typeof button.label === 'string' && button.label !== '')\n\t\t\t\t.map((button) => {\n\t\t\t\t\tconst entry = headerActionAsLink(actions.find((action) => action && action.id === button.action))\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(button.id || button.action),\n\t\t\t\t\t\tlabel: this.tr(button.label),\n\t\t\t\t\t\taction: button.action,\n\t\t\t\t\t\tvariant: button.variant === 'primary' ? 'primary' : 'secondary',\n\t\t\t\t\t\ticon: typeof button.icon === 'string' ? button.icon : '',\n\t\t\t\t\t\tlink: entry && typeof entry.href === 'string' && entry.href !== '' ? { href: entry.href, target: entry.target } : null,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The header row in the order it renders. Without the board look:\n\t\t * the declared buttons in the declared order. Under the board look:\n\t\t * the secondary buttons, the buildiq square, then the primary ones,\n\t\t * as the board draws \"Downloaden\" and the square. No declared button\n\t\t * leaves the header as it was, without the square.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @return {Array<object>} The buttons; the square is `{ key: '__buildiq', action: '__buildiq' }`.\n\t\t */\n\t\theaderButtonsRow() {\n\t\t\tconst buttons = this.resolvedHeaderButtons\n\t\t\tif (buttons.length === 0 || !this.isBoard) {\n\t\t\t\treturn buttons\n\t\t\t}\n\t\t\tconst square = { key: '__buildiq', action: '__buildiq', variant: 'secondary', label: '', icon: '' }\n\t\t\treturn [\n\t\t\t\t...buttons.filter((button) => button.variant !== 'primary'),\n\t\t\t\tsquare,\n\t\t\t\t...buttons.filter((button) => button.variant === 'primary'),\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The heading.\n\t\t *\n\t\t * @return {string} The title.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\tconst declared = this.config.title || (this.page && this.page.title)\n\n\t\t\treturn declared ? this.tr(declared) : t('nextcloud-vue', 'Reports')\n\t\t},\n\n\t\t/**\n\t\t * The optional lead paragraph.\n\t\t *\n\t\t * @return {string} The description.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The declared cards, ignoring any that name no route.\n\t\t *\n\t\t * A card with no route is a card that cannot be opened. Rendering it\n\t\t * would put a report on the page that silently does nothing, which is\n\t\t * worse than leaving it off.\n\t\t *\n\t\t * @return {Array<object>} The usable cards.\n\t\t */\n\t\tresolvedCards() {\n\t\t\tconst declared = this.config.cards\n\t\t\tif (!Array.isArray(declared)) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.route && card.label)\n\t\t\t\t.map((card) => ({\n\t\t\t\t\t...card,\n\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Category value/label pairs, offered only for categories that hold a\n\t\t * card. A filter listing empty categories teaches the reader that the\n\t\t * app has reports it does not have.\n\t\t *\n\t\t * @return {Array<object>} The options.\n\t\t */\n\t\tcategoryOptions() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst used = new Set(this.resolvedCards.map((card) => card.category).filter(Boolean))\n\t\t\tconst options = [{ value: 'all', label: this.allLabel }]\n\n\t\t\tObject.keys(declared)\n\t\t\t\t.filter((key) => used.has(key))\n\t\t\t\t.forEach((key) => options.push({ value: key, label: this.tr(declared[key]) }))\n\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * The board look's sections: the cards without a category first, under\n\t\t * no heading, then one section per declared category that holds a\n\t\t * card, in the declared order, headed by its label.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-page-board-look/specs/reports-page/spec.md#requirement-the-reports-page-takes-the-board-look\n\t\t * @return {Array<{key: string, label: string, cards: Array<object>}>} The sections.\n\t\t */\n\t\tboardSections() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst sections = []\n\t\t\tconst loose = this.resolvedCards.filter((card) => !card.category || !Object.hasOwn(declared, card.category))\n\t\t\tif (loose.length > 0) {\n\t\t\t\tsections.push({ key: '__none', label: '', cards: loose })\n\t\t\t}\n\t\t\tfor (const key of Object.keys(declared)) {\n\t\t\t\tconst cards = this.resolvedCards.filter((card) => card.category === key)\n\t\t\t\tif (cards.length > 0) {\n\t\t\t\t\tsections.push({ key, label: this.tr(declared[key]), cards })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn sections\n\t\t},\n\n\t\t/**\n\t\t * The cards the active filter admits.\n\t\t *\n\t\t * @return {Array<object>} The visible cards.\n\t\t */\n\t\tvisibleCards() {\n\t\t\tif (this.activeCategory === 'all') {\n\t\t\t\treturn this.resolvedCards\n\t\t\t}\n\n\t\t\treturn this.resolvedCards.filter((card) => card.category === this.activeCategory)\n\t\t},\n\n\t\t/**\n\t\t * A stable id for the filter control's label association.\n\t\t *\n\t\t * @return {string} The id.\n\t\t */\n\t\tfilterId() {\n\t\t\treturn `cn-reports-category-${(this.page && this.page.id) || 'page'}`\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The \"all categories\" option label.\n\t\t */\n\t\tallLabel() {\n\t\t\treturn this.config.allCategoriesLabel\n\t\t\t\t? this.tr(this.config.allCategoriesLabel)\n\t\t\t\t: t('nextcloud-vue', 'All categories')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The filter's label.\n\t\t */\n\t\tresolvedCategoryLabel() {\n\t\t\treturn this.config.categoryLabel\n\t\t\t\t? this.tr(this.config.categoryLabel)\n\t\t\t\t: t('nextcloud-vue', 'Category')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The empty-state text.\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel\n\t\t\t\t? this.tr(this.config.emptyLabel)\n\t\t\t\t: t('nextcloud-vue', 'No reports in this category.')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest-declared string through the app's own\n\t\t * translate function.\n\t\t *\n\t\t * The manifest is data the renderer walks, not source the extractor\n\t\t * reads, so every string it carries has to be handed to the app's\n\t\t * translator explicitly. A card rendered straight from the manifest\n\t\t * shows its source language whatever locale the reader is in.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translated string, or the input unchanged.\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\treturn this.effectiveTranslate(value)\n\t\t},\n\n\t\t/**\n\t\t * The human label for a card's category, if it has one.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The label, or an empty string.\n\t\t */\n\t\tcategoryName(card) {\n\t\t\tconst declared = this.config.categories || {}\n\n\t\t\treturn this.tr((card.category && declared[card.category]) || '')\n\t\t},\n\n\t\t/**\n\t\t * A real href for the card, so it is a link a reader can middle-click\n\t\t * or open in a new tab rather than a div that only answers to a click.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The href.\n\t\t */\n\t\threfFor(card) {\n\t\t\tif (!this.$router) {\n\t\t\t\treturn '#'\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn this.$router.resolve({ name: card.route }).href\n\t\t\t} catch {\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A header button was clicked: run the `headerActions` entry it names\n\t\t * and emit `header-action`, as an index page's header button does. A\n\t\t * button that is a link is left to the browser.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing\n\t\t * @param {{action: string, link?: object}} button The resolved button.\n\t\t * @return {void}\n\t\t */\n\t\tonHeaderButton(button) {\n\t\t\tif (button.link) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst id = button.action\n\t\t\tconst actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : []\n\t\t\tconst entry = actions.find((action) => action && action.id === id) || null\n\t\t\tconst handler = entry ? entry.handler : undefined\n\t\t\tif (handler === 'none') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof handler === 'function') {\n\t\t\t\thandler({ actionId: id })\n\t\t\t} else if (handler === 'navigate' && entry.route) {\n\t\t\t\tconst params = (entry.params && typeof entry.params === 'object') ? entry.params : null\n\t\t\t\tif (this.$router && typeof this.$router.push === 'function') {\n\t\t\t\t\tthis.$router.push(params ? { name: entry.route, params } : { name: entry.route })\n\t\t\t\t}\n\t\t\t} else if (entry && entry.type === 'navigate' && typeof entry.target === 'string' && entry.target !== '') {\n\t\t\t\twindow.open(entry.target, '_blank', 'noopener,noreferrer')\n\t\t\t} else if (typeof handler === 'string' && handler !== 'emit') {\n\t\t\t\tconst resolved = resolveRegisteredHandler(handler, this.cnRegistry, this.cnCustomComponents)\n\t\t\t\tif (typeof resolved === 'function') {\n\t\t\t\t\tresolved({ actionId: id })\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * A header button was clicked (`headerButtons`), after its\n\t\t\t * `headerActions` entry ran; not for `handler: \"none\"`.\n\t\t\t *\n\t\t\t * @event header-action\n\t\t\t * @property {{action: string, id: string}} payload The id of the `headerActions` entry the button names, as `action` and `id`.\n\t\t\t */\n\t\t\tthis.$emit('header-action', { action: id, id })\n\t\t},\n\n\t\t/**\n\t\t * Navigate by route NAME on a plain click; a modified or middle click\n\t\t * is left to the browser so it can open the card's href elsewhere.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t */\n\t\topen(card, event) {\n\t\t\tfollowLinkClick(event, { name: card.route }, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-reports-page {\n\tpadding: 16px;\n}\n\n/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n positioned at the left edge of .app-content) plus 12px breathing room, as\n CnPageHeader does. Only the header shifts; the body keeps the full width. */\n.cn-reports-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-reports-page__title {\n\tmargin: 0 0 4px;\n}\n\n.cn-reports-page__heading-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-start;\n\tjustify-content: space-between;\n\tgap: 12px 24px;\n}\n\n.cn-reports-page__heading-row .cn-reports-page__title {\n\tflex: 1 1 320px;\n\tmin-width: 0;\n}\n\n.cn-reports-page__header-buttons {\n\tdisplay: flex;\n\tflex: none;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 10px;\n}\n\n.cn-reports-page__description {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__filter {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 16px;\n}\n\n.cn-reports-page__filter-select {\n\tmin-width: 220px;\n}\n\n.cn-reports-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 12px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-reports-page__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\theight: 100%;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-reports-page__card:hover,\n.cn-reports-page__card:focus {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-reports-page__card-title {\n\tfont-weight: bold;\n}\n\n.cn-reports-page__card-description,\n.cn-reports-page__card-category {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-reports-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__widgets {\n\tmargin-top: 20px;\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_createElementVNode","_openBlock","_toDisplayString","_Fragment","_renderList","_createBlock","_createVNode"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import script from './CnSearchSelect.vue2.js';
|
|
|
2
2
|
import { render } from './CnSearchSelect.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-aaa45660";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|