@conduction/nextcloud-vue 2.92.0 → 2.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +57 -4
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +44 -17
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +44 -2
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +140 -59
- 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 +20 -6
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +3 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +2 -36
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +5 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -4
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +46 -0
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js +6 -2
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +82 -9
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +104 -90
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +9 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js.map +1 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +355 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +462 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +21 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +402 -391
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
- package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +22 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +20 -14
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue.js +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +2 -0
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +15 -5
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue.js +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js +8 -30
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js.map +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue3.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +9 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js.map +1 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +164 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +93 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +12 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +24 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +24 -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 +887 -247
- package/dist/esm/utils/featureCatalog.js +135 -0
- package/dist/esm/utils/featureCatalog.js.map +1 -0
- package/dist/esm/utils/fillTemplate.js +74 -3
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/headerMeta.js +62 -12
- package/dist/esm/utils/headerMeta.js.map +1 -1
- package/dist/esm/utils/roadmapIssues.js +86 -0
- package/dist/esm/utils/roadmapIssues.js.map +1 -0
- package/dist/esm/utils/userDisplayName.js +47 -0
- package/dist/esm/utils/userDisplayName.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +14820 -12966
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +887 -247
- package/l10n/en.json +24 -1
- package/l10n/nl.json +24 -1
- package/package.json +1 -1
- package/src/components/CnAppRoot/CnAppRoot.md +2 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +66 -5
- package/src/components/CnChoiceCards/CnChoiceCards.md +27 -0
- package/src/components/CnChoiceCards/CnChoiceCards.vue +168 -3
- package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -6
- package/src/components/CnDataTable/CnDataTable.vue +2 -36
- package/src/components/CnDetailPage/CnDetailPage.vue +5 -3
- package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
- package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +51 -1
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +98 -13
- package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +1053 -0
- package/src/components/CnFormDialog/CnFormDialog.vue +606 -562
- package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +23 -2
- package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +5 -0
- package/src/components/CnRoadmapTab/CnRoadmapTab.vue +6 -28
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +305 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +12 -0
- package/src/css/dialog.css +47 -0
- package/src/css/look-board-detail.css +8 -0
- package/src/css/look-board.css +33 -0
- package/src/schemas/app-manifest-v2.schema.json +9 -3
- package/src/utils/featureCatalog.js +132 -0
- package/src/utils/fillTemplate.js +73 -2
- package/src/utils/headerMeta.js +62 -12
- package/src/utils/roadmapIssues.js +82 -0
- package/src/utils/userDisplayName.js +54 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -2,7 +2,7 @@ import script from './CnNotificationPreferences.vue2.js';
|
|
|
2
2
|
import { render } from './CnNotificationPreferences.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-c746b496";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -29,6 +29,8 @@ var script = {
|
|
|
29
29
|
* than an always-empty pane.
|
|
30
30
|
*/
|
|
31
31
|
cnAppId: { default: () => '' },
|
|
32
|
+
/** The app's look; `board` gives the section its navigation icon. */
|
|
33
|
+
cnLook: { default: 'nextcloud' },
|
|
32
34
|
/**
|
|
33
35
|
* App-supplied labels for its notification rules, provided by the
|
|
34
36
|
* CnAppRoot ancestor from its `notificationLabels` prop. Keyed
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnNotificationPreferences.vue2.js","sources":["../../../../src/components/CnNotificationPreferences/CnNotificationPreferences.vue"],"sourcesContent":["<template>\n\t<NcAppSettingsSection id=\"notifications\" :name=\"t('nextcloud-vue', 'Notifications')\">\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t<NcEmptyContent v-else-if=\"error\"\n\t\t\t:name=\"t('nextcloud-vue', 'Notification preferences unavailable')\"\n\t\t\t:description=\"t('nextcloud-vue', 'Notification preferences are provided by OpenRegister and could not be loaded here.')\">\n\t\t\t<template #icon>\n\t\t\t\t<BellOffOutline :size=\"20\" />\n\t\t\t</template>\n\t\t</NcEmptyContent>\n\n\t\t<NcEmptyContent v-else-if=\"scopedEntries.length === 0\"\n\t\t\t:name=\"t('nextcloud-vue', 'No notifications')\"\n\t\t\t:description=\"t('nextcloud-vue', 'This app has no notifications.')\">\n\t\t\t<template #icon>\n\t\t\t\t<BellOutline :size=\"20\" />\n\t\t\t</template>\n\t\t</NcEmptyContent>\n\n\t\t<div v-else class=\"cn-notif-prefs\">\n\t\t\t<p class=\"cn-notif-prefs__intro\">\n\t\t\t\t{{ t('nextcloud-vue', 'Choose which notifications you want to receive. Items you leave unchanged use the app default.') }}\n\t\t\t</p>\n\t\t\t<div v-for=\"group in groupedEntries\" :key=\"group.schema\" class=\"cn-notif-prefs__group\">\n\t\t\t\t<h4 v-if=\"groupedEntries.length > 1\" class=\"cn-notif-prefs__group-title\">\n\t\t\t\t\t{{ group.schemaTitle }}\n\t\t\t\t</h4>\n\t\t\t\t<div class=\"cn-notif-prefs__grid\">\n\t\t\t\t\t<div v-for=\"entry in group.items\" :key=\"entry.key\" class=\"cn-notif-prefs__card\">\n\t\t\t\t\t\t<NcCheckboxRadioSwitch type=\"switch\"\n\t\t\t\t\t\t\t:modelValue=\"entry.enabled\"\n\t\t\t\t\t\t\t:disabled=\"entry.saving\"\n\t\t\t\t\t\t\t@update:modelValue=\"onToggle(entry, $event)\">\n\t\t\t\t\t\t\t{{ notificationLabel(entry) }}\n\t\t\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t\t\t<NcButton v-if=\"entry.source === 'user-override'\"\n\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t:disabled=\"entry.saving\"\n\t\t\t\t\t\t\t@click=\"onReset(entry)\">\n\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Reset to default') }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</NcAppSettingsSection>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { getLanguage, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\n/**\n * CnNotificationPreferences\n *\n * A self-contained `NcAppSettingsSection` that lets the current user turn\n * the notifications declared by their accessible schemas on or off. It reads\n * the effective preferences (schema default merged with the user's overrides)\n * and writes override-only values, both via OpenRegister's\n * `/api/notification-preferences` endpoint:\n *\n * GET → { results: [{ schema, schemaTitle, application, notification, enabled, channels, source }], total }\n * PUT → { schema, notification, enabled } | { schema, notification, reset: true }\n *\n * Preferences are override-only: leaving an item unchanged keeps the app\n * default, so apps that add new schemas/notifications keep working without\n * any per-user migration. Designed to render inside CnAppRoot's\n * `#user-settings` slot (its default fallback mounts this component).\n *\n * The API is multi-tenant per USER (every schema the user can read), not\n * per APP, so the raw `results` list mixes notifications from every app the\n * user has access to (e.g. \"Data-Subject Request\", \"Subsidie\",\n * \"Applicatieversie\" side by side). This component scopes the list down to\n * the app whose settings modal is currently open by matching each entry's\n * `application` field (the owning app id OpenRegister recorded on the\n * schema, e.g. \"pipelinq\") against `cnAppId`, injected by the CnAppRoot\n * ancestor. Entries with no known owning app (`application` is null/empty —\n * legacy or hand-authored schemas) are excluded from the scoped list rather\n * than guessed into the wrong app.\n */\nimport { NcAppSettingsSection, NcButton, NcCheckboxRadioSwitch, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport BellOffOutline from 'vue-material-design-icons/BellOffOutline.vue'\nimport BellOutline from 'vue-material-design-icons/BellOutline.vue'\nimport { notificationRuleLabel } from '../../utils/notificationRuleLabel.js'\n\nconst PREFS_PATH = '/apps/openregister/api/notification-preferences'\n\nexport default {\n\tname: 'CnNotificationPreferences',\n\tcomponents: {\n\t\tNcAppSettingsSection,\n\t\tNcButton,\n\t\tNcCheckboxRadioSwitch,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tBellOutline,\n\t\tBellOffOutline,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Consuming app's id (e.g. \"pipelinq\"), provided by the CnAppRoot\n\t\t * ancestor. Used to scope the notification list to the app whose\n\t\t * settings modal is currently open. Empty when no CnAppRoot ancestor\n\t\t * exists, in which case the full (unscoped) list is shown rather\n\t\t * than an always-empty pane.\n\t\t */\n\t\tcnAppId: { default: () => '' },\n\t\t/**\n\t\t * App-supplied labels for its notification rules, provided by the\n\t\t * CnAppRoot ancestor from its `notificationLabels` prop. Keyed\n\t\t * `<schema>.<key>` or `<key>`.\n\t\t */\n\t\tcnNotificationLabels: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Labels for the notification rules, keyed `<schema>.<key>` or\n\t\t * `<key>`; values are strings or per-locale maps (`{ nl, en }`).\n\t\t * Optional: inside CnAppRoot the app's `notificationLabels` arrive by\n\t\t * inject, and a rule without a label still reads as words, never as\n\t\t * its raw key. A prop given here wins over the injected map.\n\t\t *\n\t\t * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md\n\t\t */\n\t\tlabels: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\terror: false,\n\t\t\tentries: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Entries scoped to the current app. Falls back to the full list\n\t\t * when there is no `cnAppId` context to scope by (e.g. the\n\t\t * component is used standalone, outside CnAppRoot).\n\t\t *\n\t\t * @return {Array<object>} The app-scoped preference entries.\n\t\t */\n\t\tscopedEntries() {\n\t\t\tif (!this.cnAppId) {\n\t\t\t\treturn this.entries\n\t\t\t}\n\t\t\treturn this.entries.filter((entry) => entry.application === this.cnAppId)\n\t\t},\n\n\t\t/**\n\t\t * Group the scoped effective-preference list by schema for display.\n\t\t *\n\t\t * @return {Array<{schema: string, schemaTitle: string, items: Array}>} Grouped entries.\n\t\t */\n\t\tgroupedEntries() {\n\t\t\tconst groups = new Map()\n\t\t\tfor (const entry of this.scopedEntries) {\n\t\t\t\tif (!groups.has(entry.schema)) {\n\t\t\t\t\tgroups.set(entry.schema, {\n\t\t\t\t\t\tschema: entry.schema,\n\t\t\t\t\t\tschemaTitle: entry.schemaTitle || entry.schema,\n\t\t\t\t\t\titems: [],\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tgroups.get(entry.schema).items.push(entry)\n\t\t\t}\n\t\t\treturn Array.from(groups.values())\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.load()\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Load the current user's effective notification preferences.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tthis.loading = true\n\t\t\tthis.error = false\n\t\t\ttry {\n\t\t\t\tconst { data } = await axios.get(generateUrl(PREFS_PATH))\n\t\t\t\tconst results = Array.isArray(data?.results) ? data.results : []\n\t\t\t\tthis.entries = results.map((e) => ({\n\t\t\t\t\t...e,\n\t\t\t\t\tkey: `${e.schema}/${e.notification}`,\n\t\t\t\t\tenabled: e.enabled === true,\n\t\t\t\t\tsaving: false,\n\t\t\t\t}))\n\t\t\t} catch {\n\t\t\t\tthis.error = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Persist an on/off override for a single notification.\n\t\t *\n\t\t * @param {object} entry The preference row.\n\t\t * @param {boolean} checked The new on/off value.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onToggle(entry, checked) {\n\t\t\tconst previous = entry.enabled\n\t\t\tentry.saving = true\n\t\t\tentry.enabled = checked\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(PREFS_PATH), {\n\t\t\t\t\tschema: entry.schema,\n\t\t\t\t\tnotification: entry.notification,\n\t\t\t\t\tenabled: checked,\n\t\t\t\t})\n\t\t\t\tentry.source = 'user-override'\n\t\t\t} catch {\n\t\t\t\tentry.enabled = previous\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not save notification preference'))\n\t\t\t} finally {\n\t\t\t\tentry.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Clear a user override so the schema default applies again.\n\t\t *\n\t\t * @param {object} entry The preference row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onReset(entry) {\n\t\t\tentry.saving = true\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(PREFS_PATH), {\n\t\t\t\t\tschema: entry.schema,\n\t\t\t\t\tnotification: entry.notification,\n\t\t\t\t\treset: true,\n\t\t\t\t})\n\t\t\t\tawait this.load()\n\t\t\t} catch {\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not reset notification preference'))\n\t\t\t\tentry.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Human-readable label for a notification rule. Never the raw key: the\n\t\t * rule's own label, then the app's, then the library's wording for the\n\t\t * generic keys, then the key split into words.\n\t\t *\n\t\t * @param {object} entry The preference row.\n\t\t * @return {string} A display label.\n\t\t *\n\t\t * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md\n\t\t */\n\t\tnotificationLabel(entry) {\n\t\t\tconst known = {\n\t\t\t\tobject_created: t('nextcloud-vue', 'When an item is created'),\n\t\t\t\tobject_updated: t('nextcloud-vue', 'When an item is updated'),\n\t\t\t\tobject_transitioned: t('nextcloud-vue', 'When an item is assigned or changes status'),\n\t\t\t}\n\t\t\tconst language = this.currentLanguage()\n\t\t\tconst injected = typeof this.cnNotificationLabels === 'function'\n\t\t\t\t? this.cnNotificationLabels()\n\t\t\t\t: this.cnNotificationLabels\n\t\t\treturn notificationRuleLabel(entry, {\n\t\t\t\tlabels: { ...(injected || {}), ...(this.labels || {}) },\n\t\t\t\tknown,\n\t\t\t\tlanguage,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The person's language, `en` when it cannot be read.\n\t\t *\n\t\t * @return {string} The language code.\n\t\t */\n\t\tcurrentLanguage() {\n\t\t\ttry {\n\t\t\t\treturn (typeof getLanguage === 'function' && getLanguage()) || 'en'\n\t\t\t} catch {\n\t\t\t\treturn 'en'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Surface an error toast (lazy-loaded like the rest of the lib).\n\t\t *\n\t\t * @param {string} message The message to show.\n\t\t * @return {void}\n\t\t */\n\t\tshowError(message) {\n\t\t\timport('@nextcloud/dialogs').then(({ showError }) => showError(message))\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-notif-prefs__intro {\n\tmargin-bottom: 12px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-notif-prefs__group {\n\tmargin-bottom: 20px;\n}\n\n.cn-notif-prefs__group-title {\n\tmargin: 8px 0 8px;\n\tfont-weight: bold;\n}\n\n.cn-notif-prefs__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 8px;\n}\n\n.cn-notif-prefs__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: flex-start;\n\tgap: 4px;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tbackground-color: var(--color-background-hover);\n}\n</style>\n"],"names":["t","axios"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnNotificationPreferences.vue2.js","sources":["../../../../src/components/CnNotificationPreferences/CnNotificationPreferences.vue"],"sourcesContent":["<template>\n\t<NcAppSettingsSection id=\"notifications\" :name=\"t('nextcloud-vue', 'Notifications')\">\n\t\t<template v-if=\"cnLook === 'board'\" #icon>\n\t\t\t<BellOutline :size=\"18\" />\n\t\t</template>\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t<NcEmptyContent v-else-if=\"error\"\n\t\t\t:name=\"t('nextcloud-vue', 'Notification preferences unavailable')\"\n\t\t\t:description=\"t('nextcloud-vue', 'Notification preferences are provided by OpenRegister and could not be loaded here.')\">\n\t\t\t<template #icon>\n\t\t\t\t<BellOffOutline :size=\"20\" />\n\t\t\t</template>\n\t\t</NcEmptyContent>\n\n\t\t<NcEmptyContent v-else-if=\"scopedEntries.length === 0\"\n\t\t\t:name=\"t('nextcloud-vue', 'No notifications')\"\n\t\t\t:description=\"t('nextcloud-vue', 'This app has no notifications.')\">\n\t\t\t<template #icon>\n\t\t\t\t<BellOutline :size=\"20\" />\n\t\t\t</template>\n\t\t</NcEmptyContent>\n\n\t\t<div v-else class=\"cn-notif-prefs\">\n\t\t\t<p class=\"cn-notif-prefs__intro\">\n\t\t\t\t{{ t('nextcloud-vue', 'Choose which notifications you want to receive. Items you leave unchanged use the app default.') }}\n\t\t\t</p>\n\t\t\t<div v-for=\"group in groupedEntries\" :key=\"group.schema\" class=\"cn-notif-prefs__group\">\n\t\t\t\t<h4 v-if=\"groupedEntries.length > 1\" class=\"cn-notif-prefs__group-title\">\n\t\t\t\t\t{{ group.schemaTitle }}\n\t\t\t\t</h4>\n\t\t\t\t<div class=\"cn-notif-prefs__grid\">\n\t\t\t\t\t<div v-for=\"entry in group.items\" :key=\"entry.key\" class=\"cn-notif-prefs__card\">\n\t\t\t\t\t\t<NcCheckboxRadioSwitch type=\"switch\"\n\t\t\t\t\t\t\t:modelValue=\"entry.enabled\"\n\t\t\t\t\t\t\t:disabled=\"entry.saving\"\n\t\t\t\t\t\t\t@update:modelValue=\"onToggle(entry, $event)\">\n\t\t\t\t\t\t\t{{ notificationLabel(entry) }}\n\t\t\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t\t\t<NcButton v-if=\"entry.source === 'user-override'\"\n\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t:disabled=\"entry.saving\"\n\t\t\t\t\t\t\t@click=\"onReset(entry)\">\n\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Reset to default') }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</NcAppSettingsSection>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { getLanguage, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\n/**\n * CnNotificationPreferences\n *\n * A self-contained `NcAppSettingsSection` that lets the current user turn\n * the notifications declared by their accessible schemas on or off. It reads\n * the effective preferences (schema default merged with the user's overrides)\n * and writes override-only values, both via OpenRegister's\n * `/api/notification-preferences` endpoint:\n *\n * GET → { results: [{ schema, schemaTitle, application, notification, enabled, channels, source }], total }\n * PUT → { schema, notification, enabled } | { schema, notification, reset: true }\n *\n * Preferences are override-only: leaving an item unchanged keeps the app\n * default, so apps that add new schemas/notifications keep working without\n * any per-user migration. Designed to render inside CnAppRoot's\n * `#user-settings` slot (its default fallback mounts this component).\n *\n * The API is multi-tenant per USER (every schema the user can read), not\n * per APP, so the raw `results` list mixes notifications from every app the\n * user has access to (e.g. \"Data-Subject Request\", \"Subsidie\",\n * \"Applicatieversie\" side by side). This component scopes the list down to\n * the app whose settings modal is currently open by matching each entry's\n * `application` field (the owning app id OpenRegister recorded on the\n * schema, e.g. \"pipelinq\") against `cnAppId`, injected by the CnAppRoot\n * ancestor. Entries with no known owning app (`application` is null/empty —\n * legacy or hand-authored schemas) are excluded from the scoped list rather\n * than guessed into the wrong app.\n */\nimport { NcAppSettingsSection, NcButton, NcCheckboxRadioSwitch, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport BellOffOutline from 'vue-material-design-icons/BellOffOutline.vue'\nimport BellOutline from 'vue-material-design-icons/BellOutline.vue'\nimport { notificationRuleLabel } from '../../utils/notificationRuleLabel.js'\n\nconst PREFS_PATH = '/apps/openregister/api/notification-preferences'\n\nexport default {\n\tname: 'CnNotificationPreferences',\n\tcomponents: {\n\t\tNcAppSettingsSection,\n\t\tNcButton,\n\t\tNcCheckboxRadioSwitch,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tBellOutline,\n\t\tBellOffOutline,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Consuming app's id (e.g. \"pipelinq\"), provided by the CnAppRoot\n\t\t * ancestor. Used to scope the notification list to the app whose\n\t\t * settings modal is currently open. Empty when no CnAppRoot ancestor\n\t\t * exists, in which case the full (unscoped) list is shown rather\n\t\t * than an always-empty pane.\n\t\t */\n\t\tcnAppId: { default: () => '' },\n\t\t/** The app's look; `board` gives the section its navigation icon. */\n\t\tcnLook: { default: 'nextcloud' },\n\t\t/**\n\t\t * App-supplied labels for its notification rules, provided by the\n\t\t * CnAppRoot ancestor from its `notificationLabels` prop. Keyed\n\t\t * `<schema>.<key>` or `<key>`.\n\t\t */\n\t\tcnNotificationLabels: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Labels for the notification rules, keyed `<schema>.<key>` or\n\t\t * `<key>`; values are strings or per-locale maps (`{ nl, en }`).\n\t\t * Optional: inside CnAppRoot the app's `notificationLabels` arrive by\n\t\t * inject, and a rule without a label still reads as words, never as\n\t\t * its raw key. A prop given here wins over the injected map.\n\t\t *\n\t\t * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md\n\t\t */\n\t\tlabels: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\terror: false,\n\t\t\tentries: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Entries scoped to the current app. Falls back to the full list\n\t\t * when there is no `cnAppId` context to scope by (e.g. the\n\t\t * component is used standalone, outside CnAppRoot).\n\t\t *\n\t\t * @return {Array<object>} The app-scoped preference entries.\n\t\t */\n\t\tscopedEntries() {\n\t\t\tif (!this.cnAppId) {\n\t\t\t\treturn this.entries\n\t\t\t}\n\t\t\treturn this.entries.filter((entry) => entry.application === this.cnAppId)\n\t\t},\n\n\t\t/**\n\t\t * Group the scoped effective-preference list by schema for display.\n\t\t *\n\t\t * @return {Array<{schema: string, schemaTitle: string, items: Array}>} Grouped entries.\n\t\t */\n\t\tgroupedEntries() {\n\t\t\tconst groups = new Map()\n\t\t\tfor (const entry of this.scopedEntries) {\n\t\t\t\tif (!groups.has(entry.schema)) {\n\t\t\t\t\tgroups.set(entry.schema, {\n\t\t\t\t\t\tschema: entry.schema,\n\t\t\t\t\t\tschemaTitle: entry.schemaTitle || entry.schema,\n\t\t\t\t\t\titems: [],\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tgroups.get(entry.schema).items.push(entry)\n\t\t\t}\n\t\t\treturn Array.from(groups.values())\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.load()\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Load the current user's effective notification preferences.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tthis.loading = true\n\t\t\tthis.error = false\n\t\t\ttry {\n\t\t\t\tconst { data } = await axios.get(generateUrl(PREFS_PATH))\n\t\t\t\tconst results = Array.isArray(data?.results) ? data.results : []\n\t\t\t\tthis.entries = results.map((e) => ({\n\t\t\t\t\t...e,\n\t\t\t\t\tkey: `${e.schema}/${e.notification}`,\n\t\t\t\t\tenabled: e.enabled === true,\n\t\t\t\t\tsaving: false,\n\t\t\t\t}))\n\t\t\t} catch {\n\t\t\t\tthis.error = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Persist an on/off override for a single notification.\n\t\t *\n\t\t * @param {object} entry The preference row.\n\t\t * @param {boolean} checked The new on/off value.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onToggle(entry, checked) {\n\t\t\tconst previous = entry.enabled\n\t\t\tentry.saving = true\n\t\t\tentry.enabled = checked\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(PREFS_PATH), {\n\t\t\t\t\tschema: entry.schema,\n\t\t\t\t\tnotification: entry.notification,\n\t\t\t\t\tenabled: checked,\n\t\t\t\t})\n\t\t\t\tentry.source = 'user-override'\n\t\t\t} catch {\n\t\t\t\tentry.enabled = previous\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not save notification preference'))\n\t\t\t} finally {\n\t\t\t\tentry.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Clear a user override so the schema default applies again.\n\t\t *\n\t\t * @param {object} entry The preference row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onReset(entry) {\n\t\t\tentry.saving = true\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(PREFS_PATH), {\n\t\t\t\t\tschema: entry.schema,\n\t\t\t\t\tnotification: entry.notification,\n\t\t\t\t\treset: true,\n\t\t\t\t})\n\t\t\t\tawait this.load()\n\t\t\t} catch {\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not reset notification preference'))\n\t\t\t\tentry.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Human-readable label for a notification rule. Never the raw key: the\n\t\t * rule's own label, then the app's, then the library's wording for the\n\t\t * generic keys, then the key split into words.\n\t\t *\n\t\t * @param {object} entry The preference row.\n\t\t * @return {string} A display label.\n\t\t *\n\t\t * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md\n\t\t */\n\t\tnotificationLabel(entry) {\n\t\t\tconst known = {\n\t\t\t\tobject_created: t('nextcloud-vue', 'When an item is created'),\n\t\t\t\tobject_updated: t('nextcloud-vue', 'When an item is updated'),\n\t\t\t\tobject_transitioned: t('nextcloud-vue', 'When an item is assigned or changes status'),\n\t\t\t}\n\t\t\tconst language = this.currentLanguage()\n\t\t\tconst injected = typeof this.cnNotificationLabels === 'function'\n\t\t\t\t? this.cnNotificationLabels()\n\t\t\t\t: this.cnNotificationLabels\n\t\t\treturn notificationRuleLabel(entry, {\n\t\t\t\tlabels: { ...(injected || {}), ...(this.labels || {}) },\n\t\t\t\tknown,\n\t\t\t\tlanguage,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The person's language, `en` when it cannot be read.\n\t\t *\n\t\t * @return {string} The language code.\n\t\t */\n\t\tcurrentLanguage() {\n\t\t\ttry {\n\t\t\t\treturn (typeof getLanguage === 'function' && getLanguage()) || 'en'\n\t\t\t} catch {\n\t\t\t\treturn 'en'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Surface an error toast (lazy-loaded like the rest of the lib).\n\t\t *\n\t\t * @param {string} message The message to show.\n\t\t * @return {void}\n\t\t */\n\t\tshowError(message) {\n\t\t\timport('@nextcloud/dialogs').then(({ showError }) => showError(message))\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-notif-prefs__intro {\n\tmargin-bottom: 12px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-notif-prefs__group {\n\tmargin-bottom: 20px;\n}\n\n.cn-notif-prefs__group-title {\n\tmargin: 8px 0 8px;\n\tfont-weight: bold;\n}\n\n.cn-notif-prefs__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 8px;\n}\n\n.cn-notif-prefs__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: flex-start;\n\tgap: 4px;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tbackground-color: var(--color-background-hover);\n}\n</style>\n"],"names":["t","axios"],"mappings":";;;;;;;;AAyFA,MAAM,UAAS,GAAI,kDAAgD;;AAEnE,aAAe;AACd,CAAA,IAAI,EAAE,2BAA2B;CACjC,UAAU,EAAE;AACX,EAAA,oBAAoB;AACpB,EAAA,QAAQ;AACR,EAAA,qBAAqB;AACrB,EAAA,cAAc;EACd,aAAa;EACb,WAAW;AACX,EAAA,cAAc;EACd;;AAED,CAAA,MAAM,EAAE;AACP;;;;;;;EAOA,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,EAAC,EAAG;;EAE9B,MAAM,EAAE,EAAE,OAAO,EAAE,WAAU,EAAG;AAChC;;;;;AAKA,EAAA,oBAAoB,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG;EAC7C;;AAED,CAAA,KAAK,EAAE;AACN;;;;;;AAMC;;;AAGD,EAAA,MAAM,EAAE;GACP,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,IAAI;GACb;EACD;;AAED,CAAA,IAAI,GAAG;AACN,EAAA,OAAO;GACN,OAAO,EAAE,IAAI;GACb,KAAK,EAAE,KAAK;GACZ,OAAO,EAAE,EAAE;AACZ,GAAA;EACA;;AAED,CAAA,QAAQ,EAAE;AACT;;;AAGC;AACA;;;AAGD,EAAA,aAAa,GAAG;AACf,GAAA,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;AAClB,IAAA,OAAO,IAAI,CAAC,OAAM;AACnB,IAAA;AACA,GAAA,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,WAAU,KAAM,IAAI,CAAC,OAAO,CAAA;GACxE;;AAED;;AAEC;AACA;;AAED,EAAA,cAAc,GAAG;AAChB,GAAA,MAAM,MAAK,GAAI,IAAI,GAAG,GAAC;AACvB,GAAA,KAAK,MAAM,KAAI,IAAK,IAAI,CAAC,aAAa,EAAE;IACvC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE;KAC9B,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE;AACxB,MAAA,MAAM,EAAE,KAAK,CAAC,MAAM;AACpB,MAAA,WAAW,EAAE,KAAK,CAAC,WAAU,IAAK,KAAK,CAAC,MAAM;AAC9C,MAAA,KAAK,EAAE,EAAE;MACT,EAAA;AACF,KAAA;AACA,IAAA,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAA;AAC1C,IAAA;AACA,GAAA,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAA;GACjC;EACD;;AAED,CAAA,OAAO,GAAG;EACT,IAAI,CAAC,IAAI,GAAC;EACV;;AAED,CAAA,OAAO,EAAE;KACRA,SAAC;;AAED;;AAEC;;;EAGD,MAAM,IAAI,GAAG;AACZ,GAAA,IAAI,CAAC,OAAM,GAAI,KAAG;AAClB,GAAA,IAAI,CAAC,KAAM,GAAE,MAAI;AACjB,GAAA,IAAI;IACH,MAAM,EAAE,IAAG,EAAI,GAAE,MAAMC,cAAK,CAAC,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,EAAA;IACxD,MAAM,OAAQ,GAAE,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAE,GAAE,IAAI,CAAC,OAAQ,GAAE,GAAC;AAC/D,IAAA,IAAI,CAAC,OAAQ,GAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM;KAClC,GAAG,CAAC;AACJ,KAAA,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC;KACpC,OAAO,EAAE,CAAC,CAAC,OAAQ,KAAI,IAAI;KAC3B,MAAM,EAAE,KAAK;AACd,KAAC,CAAC,EAAA;AACH,IAAA,CAAE,MAAM;AACP,IAAA,IAAI,CAAC,KAAM,GAAE,KAAG;AACjB,IAAA,SAAU;AACT,IAAA,IAAI,CAAC,OAAQ,GAAE,MAAI;AACpB,IAAA;GACA;;AAED;;AAEC;AACA;AACA;;;EAGD,MAAM,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE;GAC9B,MAAM,QAAO,GAAI,KAAK,CAAC,QAAM;AAC7B,GAAA,KAAK,CAAC,MAAK,GAAI,KAAG;AAClB,GAAA,KAAK,CAAC,UAAU,QAAM;AACtB,GAAA,IAAI;AACH,IAAA,MAAMA,cAAK,CAAC,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE;AACxC,KAAA,MAAM,EAAE,KAAK,CAAC,MAAM;AACpB,KAAA,YAAY,EAAE,KAAK,CAAC,YAAY;AAChC,KAAA,OAAO,EAAE,OAAO;KAChB,EAAA;IACD,KAAK,CAAC,MAAO,GAAE,gBAAc;AAC9B,IAAA,CAAE,MAAM;AACP,IAAA,KAAK,CAAC,OAAM,GAAI,SAAO;AACvB,IAAA,IAAI,CAAC,SAAS,CAACD,SAAC,CAAC,eAAe,EAAE,wCAAwC,CAAC,EAAA;AAC5E,IAAA,SAAU;AACT,IAAA,KAAK,CAAC,MAAO,GAAE,MAAI;AACpB,IAAA;GACA;;AAED;;AAEC;AACA;;;AAGD,EAAA,MAAM,OAAO,CAAC,KAAK,EAAE;AACpB,GAAA,KAAK,CAAC,MAAK,GAAI,KAAG;AAClB,GAAA,IAAI;AACH,IAAA,MAAMC,cAAK,CAAC,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE;AACxC,KAAA,MAAM,EAAE,KAAK,CAAC,MAAM;AACpB,KAAA,YAAY,EAAE,KAAK,CAAC,YAAY;KAChC,KAAK,EAAE,IAAI;KACX,EAAA;AACD,IAAA,MAAM,IAAI,CAAC,IAAI,GAAC;AACjB,IAAA,CAAE,MAAM;AACP,IAAA,IAAI,CAAC,SAAS,CAACD,SAAC,CAAC,eAAe,EAAE,yCAAyC,CAAC,EAAA;AAC5E,IAAA,KAAK,CAAC,MAAO,GAAE,MAAI;AACpB,IAAA;GACA;;AAED;;;AAGC;AACA;AACA;;AAEA;;;EAGD,iBAAiB,CAAC,KAAK,EAAE;GACxB,MAAM,KAAI,GAAI;IACb,cAAc,EAAEA,SAAC,CAAC,eAAe,EAAE,yBAAyB,CAAC;IAC7D,cAAc,EAAEA,SAAC,CAAC,eAAe,EAAE,yBAAyB,CAAC;AAC7D,IAAA,mBAAmB,EAAEA,SAAC,CAAC,eAAe,EAAE,4CAA4C,CAAC;AACtF,KAAA;AACA,GAAA,MAAM,QAAS,GAAE,IAAI,CAAC,eAAe,GAAC;GACtC,MAAM,QAAO,GAAI,OAAO,IAAI,CAAC,oBAAqB,KAAI,UAAS;MAC5D,IAAI,CAAC,oBAAoB,EAAC;MAC1B,IAAI,CAAC,qBAAmB;AAC3B,GAAA,OAAO,qBAAqB,CAAC,KAAK,EAAE;IACnC,MAAM,EAAE,EAAE,IAAI,QAAO,IAAK,EAAE,CAAC,EAAE,IAAI,IAAI,CAAC,MAAK,IAAK,EAAE,CAAA,EAAG;IACvD,KAAK;AACL,IAAA,QAAQ;IACR,CAAA;GACD;;AAED;AACC;AACA;AACA;;AAED,EAAA,eAAe,GAAG;AACjB,GAAA,IAAI;IACH,OAAO,CAAC,OAAO,WAAY,KAAI,UAAW,IAAG,WAAW,EAAE,KAAK,IAAG;AACnE,IAAA,CAAE,MAAM;AACP,IAAA,OAAO,IAAG;AACX,IAAA;GACA;;AAED;;AAEC;AACA;;;AAGD,EAAA,SAAS,CAAC,OAAO,EAAE;AAClB,GAAA,OAAO,qDAAoB,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,SAAQ,EAAG,KAAK,SAAS,CAAC,OAAO,CAAC,EAAA;GACvE;EACD;AACF;;;;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveComponent, openBlock, createBlock, withCtx, createVNode, createElementBlock, createElementVNode, toDisplayString, Fragment, renderList, createCommentVNode, createTextVNode } from 'vue';
|
|
1
|
+
import { resolveComponent, openBlock, createBlock, createSlots, withCtx, createVNode, createElementBlock, createElementVNode, toDisplayString, Fragment, renderList, createCommentVNode, createTextVNode } from 'vue';
|
|
2
2
|
|
|
3
3
|
const _hoisted_1 = {
|
|
4
4
|
key: 3,
|
|
@@ -12,10 +12,10 @@ const _hoisted_3 = {
|
|
|
12
12
|
const _hoisted_4 = { class: "cn-notif-prefs__grid" };
|
|
13
13
|
|
|
14
14
|
function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
15
|
+
const _component_BellOutline = resolveComponent("BellOutline");
|
|
15
16
|
const _component_NcLoadingIcon = resolveComponent("NcLoadingIcon");
|
|
16
17
|
const _component_BellOffOutline = resolveComponent("BellOffOutline");
|
|
17
18
|
const _component_NcEmptyContent = resolveComponent("NcEmptyContent");
|
|
18
|
-
const _component_BellOutline = resolveComponent("BellOutline");
|
|
19
19
|
const _component_NcCheckboxRadioSwitch = resolveComponent("NcCheckboxRadioSwitch");
|
|
20
20
|
const _component_NcButton = resolveComponent("NcButton");
|
|
21
21
|
const _component_NcAppSettingsSection = resolveComponent("NcAppSettingsSection");
|
|
@@ -23,7 +23,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
23
23
|
return (openBlock(), createBlock(_component_NcAppSettingsSection, {
|
|
24
24
|
id: "notifications",
|
|
25
25
|
name: $options.t('nextcloud-vue', 'Notifications')
|
|
26
|
-
}, {
|
|
26
|
+
}, createSlots({
|
|
27
27
|
default: withCtx(() => [
|
|
28
28
|
($data.loading)
|
|
29
29
|
? (openBlock(), createBlock(_component_NcLoadingIcon, {
|
|
@@ -99,8 +99,18 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
99
99
|
}), 128 /* KEYED_FRAGMENT */))
|
|
100
100
|
]))
|
|
101
101
|
]),
|
|
102
|
-
_:
|
|
103
|
-
},
|
|
102
|
+
_: 2 /* DYNAMIC */
|
|
103
|
+
}, [
|
|
104
|
+
($options.cnLook === 'board')
|
|
105
|
+
? {
|
|
106
|
+
name: "icon",
|
|
107
|
+
fn: withCtx(() => [
|
|
108
|
+
createVNode(_component_BellOutline, { size: 18 })
|
|
109
|
+
]),
|
|
110
|
+
key: "0"
|
|
111
|
+
}
|
|
112
|
+
: undefined
|
|
113
|
+
]), 1032 /* PROPS, DYNAMIC_SLOTS */, ["name"]))
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
export { render };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnNotificationPreferences.vue3.js","sources":["../../../../src/components/CnNotificationPreferences/CnNotificationPreferences.vue?vue&type=template&id=2a94b3e4&lang.js"],"sourcesContent":["<template>\n\t<NcAppSettingsSection id=\"notifications\" :name=\"t('nextcloud-vue', 'Notifications')\">\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t<NcEmptyContent v-else-if=\"error\"\n\t\t\t:name=\"t('nextcloud-vue', 'Notification preferences unavailable')\"\n\t\t\t:description=\"t('nextcloud-vue', 'Notification preferences are provided by OpenRegister and could not be loaded here.')\">\n\t\t\t<template #icon>\n\t\t\t\t<BellOffOutline :size=\"20\" />\n\t\t\t</template>\n\t\t</NcEmptyContent>\n\n\t\t<NcEmptyContent v-else-if=\"scopedEntries.length === 0\"\n\t\t\t:name=\"t('nextcloud-vue', 'No notifications')\"\n\t\t\t:description=\"t('nextcloud-vue', 'This app has no notifications.')\">\n\t\t\t<template #icon>\n\t\t\t\t<BellOutline :size=\"20\" />\n\t\t\t</template>\n\t\t</NcEmptyContent>\n\n\t\t<div v-else class=\"cn-notif-prefs\">\n\t\t\t<p class=\"cn-notif-prefs__intro\">\n\t\t\t\t{{ t('nextcloud-vue', 'Choose which notifications you want to receive. Items you leave unchanged use the app default.') }}\n\t\t\t</p>\n\t\t\t<div v-for=\"group in groupedEntries\" :key=\"group.schema\" class=\"cn-notif-prefs__group\">\n\t\t\t\t<h4 v-if=\"groupedEntries.length > 1\" class=\"cn-notif-prefs__group-title\">\n\t\t\t\t\t{{ group.schemaTitle }}\n\t\t\t\t</h4>\n\t\t\t\t<div class=\"cn-notif-prefs__grid\">\n\t\t\t\t\t<div v-for=\"entry in group.items\" :key=\"entry.key\" class=\"cn-notif-prefs__card\">\n\t\t\t\t\t\t<NcCheckboxRadioSwitch type=\"switch\"\n\t\t\t\t\t\t\t:modelValue=\"entry.enabled\"\n\t\t\t\t\t\t\t:disabled=\"entry.saving\"\n\t\t\t\t\t\t\t@update:modelValue=\"onToggle(entry, $event)\">\n\t\t\t\t\t\t\t{{ notificationLabel(entry) }}\n\t\t\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t\t\t<NcButton v-if=\"entry.source === 'user-override'\"\n\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t:disabled=\"entry.saving\"\n\t\t\t\t\t\t\t@click=\"onReset(entry)\">\n\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Reset to default') }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</NcAppSettingsSection>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { getLanguage, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\n/**\n * CnNotificationPreferences\n *\n * A self-contained `NcAppSettingsSection` that lets the current user turn\n * the notifications declared by their accessible schemas on or off. It reads\n * the effective preferences (schema default merged with the user's overrides)\n * and writes override-only values, both via OpenRegister's\n * `/api/notification-preferences` endpoint:\n *\n * GET → { results: [{ schema, schemaTitle, application, notification, enabled, channels, source }], total }\n * PUT → { schema, notification, enabled } | { schema, notification, reset: true }\n *\n * Preferences are override-only: leaving an item unchanged keeps the app\n * default, so apps that add new schemas/notifications keep working without\n * any per-user migration. Designed to render inside CnAppRoot's\n * `#user-settings` slot (its default fallback mounts this component).\n *\n * The API is multi-tenant per USER (every schema the user can read), not\n * per APP, so the raw `results` list mixes notifications from every app the\n * user has access to (e.g. \"Data-Subject Request\", \"Subsidie\",\n * \"Applicatieversie\" side by side). This component scopes the list down to\n * the app whose settings modal is currently open by matching each entry's\n * `application` field (the owning app id OpenRegister recorded on the\n * schema, e.g. \"pipelinq\") against `cnAppId`, injected by the CnAppRoot\n * ancestor. Entries with no known owning app (`application` is null/empty —\n * legacy or hand-authored schemas) are excluded from the scoped list rather\n * than guessed into the wrong app.\n */\nimport { NcAppSettingsSection, NcButton, NcCheckboxRadioSwitch, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport BellOffOutline from 'vue-material-design-icons/BellOffOutline.vue'\nimport BellOutline from 'vue-material-design-icons/BellOutline.vue'\nimport { notificationRuleLabel } from '../../utils/notificationRuleLabel.js'\n\nconst PREFS_PATH = '/apps/openregister/api/notification-preferences'\n\nexport default {\n\tname: 'CnNotificationPreferences',\n\tcomponents: {\n\t\tNcAppSettingsSection,\n\t\tNcButton,\n\t\tNcCheckboxRadioSwitch,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tBellOutline,\n\t\tBellOffOutline,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Consuming app's id (e.g. \"pipelinq\"), provided by the CnAppRoot\n\t\t * ancestor. Used to scope the notification list to the app whose\n\t\t * settings modal is currently open. Empty when no CnAppRoot ancestor\n\t\t * exists, in which case the full (unscoped) list is shown rather\n\t\t * than an always-empty pane.\n\t\t */\n\t\tcnAppId: { default: () => '' },\n\t\t/**\n\t\t * App-supplied labels for its notification rules, provided by the\n\t\t * CnAppRoot ancestor from its `notificationLabels` prop. Keyed\n\t\t * `<schema>.<key>` or `<key>`.\n\t\t */\n\t\tcnNotificationLabels: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Labels for the notification rules, keyed `<schema>.<key>` or\n\t\t * `<key>`; values are strings or per-locale maps (`{ nl, en }`).\n\t\t * Optional: inside CnAppRoot the app's `notificationLabels` arrive by\n\t\t * inject, and a rule without a label still reads as words, never as\n\t\t * its raw key. A prop given here wins over the injected map.\n\t\t *\n\t\t * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md\n\t\t */\n\t\tlabels: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\terror: false,\n\t\t\tentries: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Entries scoped to the current app. Falls back to the full list\n\t\t * when there is no `cnAppId` context to scope by (e.g. the\n\t\t * component is used standalone, outside CnAppRoot).\n\t\t *\n\t\t * @return {Array<object>} The app-scoped preference entries.\n\t\t */\n\t\tscopedEntries() {\n\t\t\tif (!this.cnAppId) {\n\t\t\t\treturn this.entries\n\t\t\t}\n\t\t\treturn this.entries.filter((entry) => entry.application === this.cnAppId)\n\t\t},\n\n\t\t/**\n\t\t * Group the scoped effective-preference list by schema for display.\n\t\t *\n\t\t * @return {Array<{schema: string, schemaTitle: string, items: Array}>} Grouped entries.\n\t\t */\n\t\tgroupedEntries() {\n\t\t\tconst groups = new Map()\n\t\t\tfor (const entry of this.scopedEntries) {\n\t\t\t\tif (!groups.has(entry.schema)) {\n\t\t\t\t\tgroups.set(entry.schema, {\n\t\t\t\t\t\tschema: entry.schema,\n\t\t\t\t\t\tschemaTitle: entry.schemaTitle || entry.schema,\n\t\t\t\t\t\titems: [],\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tgroups.get(entry.schema).items.push(entry)\n\t\t\t}\n\t\t\treturn Array.from(groups.values())\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.load()\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Load the current user's effective notification preferences.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tthis.loading = true\n\t\t\tthis.error = false\n\t\t\ttry {\n\t\t\t\tconst { data } = await axios.get(generateUrl(PREFS_PATH))\n\t\t\t\tconst results = Array.isArray(data?.results) ? data.results : []\n\t\t\t\tthis.entries = results.map((e) => ({\n\t\t\t\t\t...e,\n\t\t\t\t\tkey: `${e.schema}/${e.notification}`,\n\t\t\t\t\tenabled: e.enabled === true,\n\t\t\t\t\tsaving: false,\n\t\t\t\t}))\n\t\t\t} catch {\n\t\t\t\tthis.error = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Persist an on/off override for a single notification.\n\t\t *\n\t\t * @param {object} entry The preference row.\n\t\t * @param {boolean} checked The new on/off value.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onToggle(entry, checked) {\n\t\t\tconst previous = entry.enabled\n\t\t\tentry.saving = true\n\t\t\tentry.enabled = checked\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(PREFS_PATH), {\n\t\t\t\t\tschema: entry.schema,\n\t\t\t\t\tnotification: entry.notification,\n\t\t\t\t\tenabled: checked,\n\t\t\t\t})\n\t\t\t\tentry.source = 'user-override'\n\t\t\t} catch {\n\t\t\t\tentry.enabled = previous\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not save notification preference'))\n\t\t\t} finally {\n\t\t\t\tentry.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Clear a user override so the schema default applies again.\n\t\t *\n\t\t * @param {object} entry The preference row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onReset(entry) {\n\t\t\tentry.saving = true\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(PREFS_PATH), {\n\t\t\t\t\tschema: entry.schema,\n\t\t\t\t\tnotification: entry.notification,\n\t\t\t\t\treset: true,\n\t\t\t\t})\n\t\t\t\tawait this.load()\n\t\t\t} catch {\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not reset notification preference'))\n\t\t\t\tentry.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Human-readable label for a notification rule. Never the raw key: the\n\t\t * rule's own label, then the app's, then the library's wording for the\n\t\t * generic keys, then the key split into words.\n\t\t *\n\t\t * @param {object} entry The preference row.\n\t\t * @return {string} A display label.\n\t\t *\n\t\t * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md\n\t\t */\n\t\tnotificationLabel(entry) {\n\t\t\tconst known = {\n\t\t\t\tobject_created: t('nextcloud-vue', 'When an item is created'),\n\t\t\t\tobject_updated: t('nextcloud-vue', 'When an item is updated'),\n\t\t\t\tobject_transitioned: t('nextcloud-vue', 'When an item is assigned or changes status'),\n\t\t\t}\n\t\t\tconst language = this.currentLanguage()\n\t\t\tconst injected = typeof this.cnNotificationLabels === 'function'\n\t\t\t\t? this.cnNotificationLabels()\n\t\t\t\t: this.cnNotificationLabels\n\t\t\treturn notificationRuleLabel(entry, {\n\t\t\t\tlabels: { ...(injected || {}), ...(this.labels || {}) },\n\t\t\t\tknown,\n\t\t\t\tlanguage,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The person's language, `en` when it cannot be read.\n\t\t *\n\t\t * @return {string} The language code.\n\t\t */\n\t\tcurrentLanguage() {\n\t\t\ttry {\n\t\t\t\treturn (typeof getLanguage === 'function' && getLanguage()) || 'en'\n\t\t\t} catch {\n\t\t\t\treturn 'en'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Surface an error toast (lazy-loaded like the rest of the lib).\n\t\t *\n\t\t * @param {string} message The message to show.\n\t\t * @return {void}\n\t\t */\n\t\tshowError(message) {\n\t\t\timport('@nextcloud/dialogs').then(({ showError }) => showError(message))\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-notif-prefs__intro {\n\tmargin-bottom: 12px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-notif-prefs__group {\n\tmargin-bottom: 20px;\n}\n\n.cn-notif-prefs__group-title {\n\tmargin: 8px 0 8px;\n\tfont-weight: bold;\n}\n\n.cn-notif-prefs__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 8px;\n}\n\n.cn-notif-prefs__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: flex-start;\n\tgap: 4px;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tbackground-color: var(--color-background-hover);\n}\n</style>\n"],"names":["_createBlock","_createVNode","_openBlock","_createElementBlock","_createElementVNode","_toDisplayString","_Fragment","_renderList","_createTextVNode"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnNotificationPreferences.vue3.js","sources":["../../../../src/components/CnNotificationPreferences/CnNotificationPreferences.vue?vue&type=template&id=c746b496&lang.js"],"sourcesContent":["<template>\n\t<NcAppSettingsSection id=\"notifications\" :name=\"t('nextcloud-vue', 'Notifications')\">\n\t\t<template v-if=\"cnLook === 'board'\" #icon>\n\t\t\t<BellOutline :size=\"18\" />\n\t\t</template>\n\t\t<NcLoadingIcon v-if=\"loading\" :size=\"24\" />\n\n\t\t<NcEmptyContent v-else-if=\"error\"\n\t\t\t:name=\"t('nextcloud-vue', 'Notification preferences unavailable')\"\n\t\t\t:description=\"t('nextcloud-vue', 'Notification preferences are provided by OpenRegister and could not be loaded here.')\">\n\t\t\t<template #icon>\n\t\t\t\t<BellOffOutline :size=\"20\" />\n\t\t\t</template>\n\t\t</NcEmptyContent>\n\n\t\t<NcEmptyContent v-else-if=\"scopedEntries.length === 0\"\n\t\t\t:name=\"t('nextcloud-vue', 'No notifications')\"\n\t\t\t:description=\"t('nextcloud-vue', 'This app has no notifications.')\">\n\t\t\t<template #icon>\n\t\t\t\t<BellOutline :size=\"20\" />\n\t\t\t</template>\n\t\t</NcEmptyContent>\n\n\t\t<div v-else class=\"cn-notif-prefs\">\n\t\t\t<p class=\"cn-notif-prefs__intro\">\n\t\t\t\t{{ t('nextcloud-vue', 'Choose which notifications you want to receive. Items you leave unchanged use the app default.') }}\n\t\t\t</p>\n\t\t\t<div v-for=\"group in groupedEntries\" :key=\"group.schema\" class=\"cn-notif-prefs__group\">\n\t\t\t\t<h4 v-if=\"groupedEntries.length > 1\" class=\"cn-notif-prefs__group-title\">\n\t\t\t\t\t{{ group.schemaTitle }}\n\t\t\t\t</h4>\n\t\t\t\t<div class=\"cn-notif-prefs__grid\">\n\t\t\t\t\t<div v-for=\"entry in group.items\" :key=\"entry.key\" class=\"cn-notif-prefs__card\">\n\t\t\t\t\t\t<NcCheckboxRadioSwitch type=\"switch\"\n\t\t\t\t\t\t\t:modelValue=\"entry.enabled\"\n\t\t\t\t\t\t\t:disabled=\"entry.saving\"\n\t\t\t\t\t\t\t@update:modelValue=\"onToggle(entry, $event)\">\n\t\t\t\t\t\t\t{{ notificationLabel(entry) }}\n\t\t\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t\t\t<NcButton v-if=\"entry.source === 'user-override'\"\n\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t:disabled=\"entry.saving\"\n\t\t\t\t\t\t\t@click=\"onReset(entry)\">\n\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Reset to default') }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</NcAppSettingsSection>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { getLanguage, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\n/**\n * CnNotificationPreferences\n *\n * A self-contained `NcAppSettingsSection` that lets the current user turn\n * the notifications declared by their accessible schemas on or off. It reads\n * the effective preferences (schema default merged with the user's overrides)\n * and writes override-only values, both via OpenRegister's\n * `/api/notification-preferences` endpoint:\n *\n * GET → { results: [{ schema, schemaTitle, application, notification, enabled, channels, source }], total }\n * PUT → { schema, notification, enabled } | { schema, notification, reset: true }\n *\n * Preferences are override-only: leaving an item unchanged keeps the app\n * default, so apps that add new schemas/notifications keep working without\n * any per-user migration. Designed to render inside CnAppRoot's\n * `#user-settings` slot (its default fallback mounts this component).\n *\n * The API is multi-tenant per USER (every schema the user can read), not\n * per APP, so the raw `results` list mixes notifications from every app the\n * user has access to (e.g. \"Data-Subject Request\", \"Subsidie\",\n * \"Applicatieversie\" side by side). This component scopes the list down to\n * the app whose settings modal is currently open by matching each entry's\n * `application` field (the owning app id OpenRegister recorded on the\n * schema, e.g. \"pipelinq\") against `cnAppId`, injected by the CnAppRoot\n * ancestor. Entries with no known owning app (`application` is null/empty —\n * legacy or hand-authored schemas) are excluded from the scoped list rather\n * than guessed into the wrong app.\n */\nimport { NcAppSettingsSection, NcButton, NcCheckboxRadioSwitch, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport BellOffOutline from 'vue-material-design-icons/BellOffOutline.vue'\nimport BellOutline from 'vue-material-design-icons/BellOutline.vue'\nimport { notificationRuleLabel } from '../../utils/notificationRuleLabel.js'\n\nconst PREFS_PATH = '/apps/openregister/api/notification-preferences'\n\nexport default {\n\tname: 'CnNotificationPreferences',\n\tcomponents: {\n\t\tNcAppSettingsSection,\n\t\tNcButton,\n\t\tNcCheckboxRadioSwitch,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tBellOutline,\n\t\tBellOffOutline,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Consuming app's id (e.g. \"pipelinq\"), provided by the CnAppRoot\n\t\t * ancestor. Used to scope the notification list to the app whose\n\t\t * settings modal is currently open. Empty when no CnAppRoot ancestor\n\t\t * exists, in which case the full (unscoped) list is shown rather\n\t\t * than an always-empty pane.\n\t\t */\n\t\tcnAppId: { default: () => '' },\n\t\t/** The app's look; `board` gives the section its navigation icon. */\n\t\tcnLook: { default: 'nextcloud' },\n\t\t/**\n\t\t * App-supplied labels for its notification rules, provided by the\n\t\t * CnAppRoot ancestor from its `notificationLabels` prop. Keyed\n\t\t * `<schema>.<key>` or `<key>`.\n\t\t */\n\t\tcnNotificationLabels: { default: () => ({}) },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Labels for the notification rules, keyed `<schema>.<key>` or\n\t\t * `<key>`; values are strings or per-locale maps (`{ nl, en }`).\n\t\t * Optional: inside CnAppRoot the app's `notificationLabels` arrive by\n\t\t * inject, and a rule without a label still reads as words, never as\n\t\t * its raw key. A prop given here wins over the injected map.\n\t\t *\n\t\t * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md\n\t\t */\n\t\tlabels: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: true,\n\t\t\terror: false,\n\t\t\tentries: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Entries scoped to the current app. Falls back to the full list\n\t\t * when there is no `cnAppId` context to scope by (e.g. the\n\t\t * component is used standalone, outside CnAppRoot).\n\t\t *\n\t\t * @return {Array<object>} The app-scoped preference entries.\n\t\t */\n\t\tscopedEntries() {\n\t\t\tif (!this.cnAppId) {\n\t\t\t\treturn this.entries\n\t\t\t}\n\t\t\treturn this.entries.filter((entry) => entry.application === this.cnAppId)\n\t\t},\n\n\t\t/**\n\t\t * Group the scoped effective-preference list by schema for display.\n\t\t *\n\t\t * @return {Array<{schema: string, schemaTitle: string, items: Array}>} Grouped entries.\n\t\t */\n\t\tgroupedEntries() {\n\t\t\tconst groups = new Map()\n\t\t\tfor (const entry of this.scopedEntries) {\n\t\t\t\tif (!groups.has(entry.schema)) {\n\t\t\t\t\tgroups.set(entry.schema, {\n\t\t\t\t\t\tschema: entry.schema,\n\t\t\t\t\t\tschemaTitle: entry.schemaTitle || entry.schema,\n\t\t\t\t\t\titems: [],\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tgroups.get(entry.schema).items.push(entry)\n\t\t\t}\n\t\t\treturn Array.from(groups.values())\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.load()\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Load the current user's effective notification preferences.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tthis.loading = true\n\t\t\tthis.error = false\n\t\t\ttry {\n\t\t\t\tconst { data } = await axios.get(generateUrl(PREFS_PATH))\n\t\t\t\tconst results = Array.isArray(data?.results) ? data.results : []\n\t\t\t\tthis.entries = results.map((e) => ({\n\t\t\t\t\t...e,\n\t\t\t\t\tkey: `${e.schema}/${e.notification}`,\n\t\t\t\t\tenabled: e.enabled === true,\n\t\t\t\t\tsaving: false,\n\t\t\t\t}))\n\t\t\t} catch {\n\t\t\t\tthis.error = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Persist an on/off override for a single notification.\n\t\t *\n\t\t * @param {object} entry The preference row.\n\t\t * @param {boolean} checked The new on/off value.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onToggle(entry, checked) {\n\t\t\tconst previous = entry.enabled\n\t\t\tentry.saving = true\n\t\t\tentry.enabled = checked\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(PREFS_PATH), {\n\t\t\t\t\tschema: entry.schema,\n\t\t\t\t\tnotification: entry.notification,\n\t\t\t\t\tenabled: checked,\n\t\t\t\t})\n\t\t\t\tentry.source = 'user-override'\n\t\t\t} catch {\n\t\t\t\tentry.enabled = previous\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not save notification preference'))\n\t\t\t} finally {\n\t\t\t\tentry.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Clear a user override so the schema default applies again.\n\t\t *\n\t\t * @param {object} entry The preference row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onReset(entry) {\n\t\t\tentry.saving = true\n\t\t\ttry {\n\t\t\t\tawait axios.put(generateUrl(PREFS_PATH), {\n\t\t\t\t\tschema: entry.schema,\n\t\t\t\t\tnotification: entry.notification,\n\t\t\t\t\treset: true,\n\t\t\t\t})\n\t\t\t\tawait this.load()\n\t\t\t} catch {\n\t\t\t\tthis.showError(t('nextcloud-vue', 'Could not reset notification preference'))\n\t\t\t\tentry.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Human-readable label for a notification rule. Never the raw key: the\n\t\t * rule's own label, then the app's, then the library's wording for the\n\t\t * generic keys, then the key split into words.\n\t\t *\n\t\t * @param {object} entry The preference row.\n\t\t * @return {string} A display label.\n\t\t *\n\t\t * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md\n\t\t */\n\t\tnotificationLabel(entry) {\n\t\t\tconst known = {\n\t\t\t\tobject_created: t('nextcloud-vue', 'When an item is created'),\n\t\t\t\tobject_updated: t('nextcloud-vue', 'When an item is updated'),\n\t\t\t\tobject_transitioned: t('nextcloud-vue', 'When an item is assigned or changes status'),\n\t\t\t}\n\t\t\tconst language = this.currentLanguage()\n\t\t\tconst injected = typeof this.cnNotificationLabels === 'function'\n\t\t\t\t? this.cnNotificationLabels()\n\t\t\t\t: this.cnNotificationLabels\n\t\t\treturn notificationRuleLabel(entry, {\n\t\t\t\tlabels: { ...(injected || {}), ...(this.labels || {}) },\n\t\t\t\tknown,\n\t\t\t\tlanguage,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The person's language, `en` when it cannot be read.\n\t\t *\n\t\t * @return {string} The language code.\n\t\t */\n\t\tcurrentLanguage() {\n\t\t\ttry {\n\t\t\t\treturn (typeof getLanguage === 'function' && getLanguage()) || 'en'\n\t\t\t} catch {\n\t\t\t\treturn 'en'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Surface an error toast (lazy-loaded like the rest of the lib).\n\t\t *\n\t\t * @param {string} message The message to show.\n\t\t * @return {void}\n\t\t */\n\t\tshowError(message) {\n\t\t\timport('@nextcloud/dialogs').then(({ showError }) => showError(message))\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-notif-prefs__intro {\n\tmargin-bottom: 12px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-notif-prefs__group {\n\tmargin-bottom: 20px;\n}\n\n.cn-notif-prefs__group-title {\n\tmargin: 8px 0 8px;\n\tfont-weight: bold;\n}\n\n.cn-notif-prefs__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 8px;\n}\n\n.cn-notif-prefs__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: flex-start;\n\tgap: 4px;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tbackground-color: var(--color-background-hover);\n}\n</style>\n"],"names":["_createBlock","_createVNode","_openBlock","_createElementBlock","_createElementVNode","_toDisplayString","_Fragment","_renderList","_createTextVNode"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import script from './CnRoadmapTab.vue2.js';
|
|
|
2
2
|
import { render } from './CnRoadmapTab.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-732ef80f";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import axios__default from '@nextcloud/axios';
|
|
2
1
|
import { translate } from '@nextcloud/l10n';
|
|
3
|
-
import { generateUrl } from '@nextcloud/router';
|
|
4
2
|
import { NcEmptyContent, NcLoadingIcon, NcButton } from '@nextcloud/vue';
|
|
5
3
|
import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue';
|
|
6
4
|
import ClockOutline from 'vue-material-design-icons/ClockOutline.vue';
|
|
7
5
|
import InformationOutline from 'vue-material-design-icons/InformationOutline.vue';
|
|
8
|
-
import
|
|
6
|
+
import KeyOutline from 'vue-material-design-icons/KeyOutline.vue';
|
|
9
7
|
import RoadVariant from 'vue-material-design-icons/RoadVariant.vue';
|
|
10
8
|
import __sfc_main from '../CnRoadmapItem/CnRoadmapItem.vue.js';
|
|
9
|
+
import { sortByReactions, fetchRoadmapIssues } from '../../utils/roadmapIssues.js';
|
|
11
10
|
|
|
12
11
|
var script = {
|
|
13
12
|
name: 'CnRoadmapTab',
|
|
@@ -16,7 +15,7 @@ var script = {
|
|
|
16
15
|
AlertCircleOutline,
|
|
17
16
|
ClockOutline,
|
|
18
17
|
InformationOutline,
|
|
19
|
-
KeyOutline
|
|
18
|
+
KeyOutline,
|
|
20
19
|
RoadVariant,
|
|
21
20
|
NcEmptyContent,
|
|
22
21
|
NcLoadingIcon,
|
|
@@ -53,7 +52,7 @@ var script = {
|
|
|
53
52
|
|
|
54
53
|
computed: {
|
|
55
54
|
sortedItems() {
|
|
56
|
-
return
|
|
55
|
+
return sortByReactions(this.items)
|
|
57
56
|
},
|
|
58
57
|
|
|
59
58
|
disabledLabel() {
|
|
@@ -104,31 +103,10 @@ var script = {
|
|
|
104
103
|
this.loading = true;
|
|
105
104
|
this.degraded = null;
|
|
106
105
|
this.items = [];
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
repo: this.repo,
|
|
112
|
-
labels: 'enhancement,feature',
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
const data = response.data || {};
|
|
116
|
-
if (data.hint === 'github_pat_not_configured') {
|
|
117
|
-
this.degraded = 'pat_not_configured';
|
|
118
|
-
} else if (Array.isArray(data.items)) {
|
|
119
|
-
this.items = data.items;
|
|
120
|
-
}
|
|
121
|
-
} catch (e) {
|
|
122
|
-
const status = e?.response?.status;
|
|
123
|
-
const code = e?.response?.data?.error;
|
|
124
|
-
if (status === 429 || code === 'github_rate_limited' || code === 'rate_limited') {
|
|
125
|
-
this.degraded = 'rate_limited';
|
|
126
|
-
} else {
|
|
127
|
-
this.degraded = 'error';
|
|
128
|
-
}
|
|
129
|
-
} finally {
|
|
130
|
-
this.loading = false;
|
|
131
|
-
}
|
|
106
|
+
const { items, degraded } = await fetchRoadmapIssues(this.repo);
|
|
107
|
+
this.items = items;
|
|
108
|
+
this.degraded = degraded;
|
|
109
|
+
this.loading = false;
|
|
132
110
|
},
|
|
133
111
|
},
|
|
134
112
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnRoadmapTab.vue2.js","sources":["../../../../src/components/CnRoadmapTab/CnRoadmapTab.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-roadmap-tab\">\n\t\t<div v-if=\"disabled\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"disabledLabel\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<InformationOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else-if=\"loading\" class=\"cn-roadmap-tab__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\t\t<div v-else-if=\"degraded === 'pat_not_configured'\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"patNotConfiguredTitle\" :description=\"patNotConfiguredDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<KeyOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else-if=\"degraded === 'rate_limited'\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"rateLimitedTitle\" :description=\"rateLimitedDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<ClockOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else-if=\"degraded === 'error'\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"errorTitle\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<AlertCircleOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t\t<template #action>\n\t\t\t\t\t<NcButton variant=\"primary\" @click=\"fetchItems\">\n\t\t\t\t\t\t{{ retryLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else-if=\"sortedItems.length === 0\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"emptyTitle\" :description=\"emptyDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<RoadVariant :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else class=\"cn-roadmap-tab__grid\">\n\t\t\t<CnRoadmapItem\n\t\t\t\tv-for=\"item in sortedItems\"\n\t\t\t\t:key=\"item.number\"\n\t\t\t\t:item=\"item\"\n\t\t\t\tclass=\"cn-roadmap-tab__card\" />\n\t\t</div>\n\t</div>\n</template>\n\n<script>\n/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n *\n * RoadmapTab — reaction-sorted GitHub issues feed. Fetches from the\n * OpenRegister `github-issue-proxy` GET endpoint on mount with the curated\n * label filter (`labels=enhancement,feature`, per openregister design D23)\n * and renders each item via `RoadmapItem`.\n *\n * Handles four documented degraded states with distinct localized messages:\n * `hint: github_pat_not_configured`, HTTP 429 `github_rate_limited`, network\n * errors, and the empty-array edge case.\n *\n * Spec: features-roadmap-component — Requirement \"RoadmapTab\".\n */\nimport
|
|
1
|
+
{"version":3,"file":"CnRoadmapTab.vue2.js","sources":["../../../../src/components/CnRoadmapTab/CnRoadmapTab.vue"],"sourcesContent":["<template>\n\t<div class=\"cn-roadmap-tab\">\n\t\t<div v-if=\"disabled\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"disabledLabel\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<InformationOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else-if=\"loading\" class=\"cn-roadmap-tab__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\t\t<div v-else-if=\"degraded === 'pat_not_configured'\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"patNotConfiguredTitle\" :description=\"patNotConfiguredDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<KeyOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else-if=\"degraded === 'rate_limited'\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"rateLimitedTitle\" :description=\"rateLimitedDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<ClockOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else-if=\"degraded === 'error'\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"errorTitle\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<AlertCircleOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t\t<template #action>\n\t\t\t\t\t<NcButton variant=\"primary\" @click=\"fetchItems\">\n\t\t\t\t\t\t{{ retryLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else-if=\"sortedItems.length === 0\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"emptyTitle\" :description=\"emptyDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<RoadVariant :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else class=\"cn-roadmap-tab__grid\">\n\t\t\t<CnRoadmapItem\n\t\t\t\tv-for=\"item in sortedItems\"\n\t\t\t\t:key=\"item.number\"\n\t\t\t\t:item=\"item\"\n\t\t\t\tclass=\"cn-roadmap-tab__card\" />\n\t\t</div>\n\t</div>\n</template>\n\n<script>\n/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n *\n * RoadmapTab — reaction-sorted GitHub issues feed. Fetches from the\n * OpenRegister `github-issue-proxy` GET endpoint on mount with the curated\n * label filter (`labels=enhancement,feature`, per openregister design D23)\n * and renders each item via `RoadmapItem`.\n *\n * Handles four documented degraded states with distinct localized messages:\n * `hint: github_pat_not_configured`, HTTP 429 `github_rate_limited`, network\n * errors, and the empty-array edge case.\n *\n * Spec: features-roadmap-component — Requirement \"RoadmapTab\".\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'\nimport ClockOutline from 'vue-material-design-icons/ClockOutline.vue'\nimport InformationOutline from 'vue-material-design-icons/InformationOutline.vue'\nimport KeyOutline from 'vue-material-design-icons/KeyOutline.vue'\nimport RoadVariant from 'vue-material-design-icons/RoadVariant.vue'\nimport CnRoadmapItem from '../CnRoadmapItem/CnRoadmapItem.vue'\nimport { fetchRoadmapIssues, sortByReactions } from '../../utils/roadmapIssues.js'\n\nexport default {\n\tname: 'CnRoadmapTab',\n\n\tcomponents: {\n\t\tAlertCircleOutline,\n\t\tClockOutline,\n\t\tInformationOutline,\n\t\tKeyOutline,\n\t\tRoadVariant,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tNcButton,\n\t\tCnRoadmapItem,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * `<owner>/<repo>` of the app's GitHub repository. Passed through\n\t\t * to the proxy as the `repo` query parameter.\n\t\t */\n\t\trepo: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * Admin opt-out flag — when true the tab renders the disabled empty state.\n\t\t */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: false,\n\t\t\titems: [],\n\t\t\tdegraded: null, // null | 'pat_not_configured' | 'rate_limited' | 'error'\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tsortedItems() {\n\t\t\treturn sortByReactions(this.items)\n\t\t},\n\n\t\tdisabledLabel() {\n\t\t\treturn t('nextcloud-vue', 'Roadmap is disabled by your administrator')\n\t\t},\n\n\t\tpatNotConfiguredTitle() {\n\t\t\treturn t('nextcloud-vue', 'Roadmap not yet configured')\n\t\t},\n\n\t\tpatNotConfiguredDescription() {\n\t\t\treturn t('nextcloud-vue', 'Ask your administrator to add a GitHub Personal Access Token in the OpenRegister settings to enable the roadmap.')\n\t\t},\n\n\t\trateLimitedTitle() {\n\t\t\treturn t('nextcloud-vue', 'GitHub temporarily unavailable')\n\t\t},\n\n\t\trateLimitedDescription() {\n\t\t\treturn t('nextcloud-vue', 'GitHub rate limited the request — please retry in a few minutes.')\n\t\t},\n\n\t\terrorTitle() {\n\t\t\treturn t('nextcloud-vue', 'Could not load the roadmap')\n\t\t},\n\n\t\temptyTitle() {\n\t\t\treturn t('nextcloud-vue', 'No roadmap items yet')\n\t\t},\n\n\t\temptyDescription() {\n\t\t\treturn t('nextcloud-vue', 'When the maintainers label issues \"enhancement\" or \"feature\" on GitHub they will appear here.')\n\t\t},\n\n\t\tretryLabel() {\n\t\t\treturn t('nextcloud-vue', 'Retry')\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (!this.disabled) {\n\t\t\tthis.fetchItems()\n\t\t}\n\t},\n\n\tmethods: {\n\t\tasync fetchItems() {\n\t\t\tthis.loading = true\n\t\t\tthis.degraded = null\n\t\t\tthis.items = []\n\t\t\tconst { items, degraded } = await fetchRoadmapIssues(this.repo)\n\t\t\tthis.items = items\n\t\t\tthis.degraded = degraded\n\t\t\tthis.loading = false\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-roadmap-tab__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n\tgap: 16px;\n}\n\n.cn-roadmap-tab__card {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\ttransition: border-color 120ms ease, box-shadow 120ms ease;\n}\n\n.cn-roadmap-tab__card:hover {\n\tborder-color: var(--color-primary-element);\n\tbox-shadow: 0 2px 8px var(--color-box-shadow, rgba(0, 0, 0, 0.08));\n}\n\n.cn-roadmap-tab__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 32px;\n}\n</style>\n"],"names":["CnRoadmapItem","t"],"mappings":";;;;;;;;;;AAiFA,aAAe;AACd,CAAA,IAAI,EAAE,cAAc;;CAEpB,UAAU,EAAE;AACX,EAAA,kBAAkB;EAClB,YAAY;AACZ,EAAA,kBAAkB;EAClB,UAAU;EACV,WAAW;AACX,EAAA,cAAc;EACd,aAAa;AACb,EAAA,QAAQ;iBACRA,UAAa;EACb;;AAED,CAAA,KAAK,EAAE;AACN;;AAEC;;EAED,IAAI,EAAE;GACL,IAAI,EAAE,MAAM;GACZ,QAAQ,EAAE,IAAI;GACd;;AAED;;;AAGA,EAAA,QAAQ,EAAE;GACT,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,KAAK;GACd;EACD;;AAED,CAAA,IAAI,GAAG;AACN,EAAA,OAAO;GACN,OAAO,EAAE,KAAK;AACd,GAAA,KAAK,EAAE,EAAE;AACT,GAAA,QAAQ,EAAE,IAAI;AACf,GAAA;EACA;;AAED,CAAA,QAAQ,EAAE;EACT,WAAW,GAAG;AACb,GAAA,OAAO,eAAe,CAAC,IAAI,CAAC,KAAK,CAAA;GACjC;;AAED,EAAA,aAAa,GAAG;AACf,GAAA,OAAOC,SAAC,CAAC,eAAe,EAAE,2CAA2C,CAAA;GACrE;;EAED,qBAAqB,GAAG;GACvB,OAAOA,SAAC,CAAC,eAAe,EAAE,4BAA4B,CAAA;GACtD;;EAED,2BAA2B,GAAG;GAC7B,OAAOA,SAAC,CAAC,eAAe,EAAE,kHAAkH,CAAA;GAC5I;;AAED,EAAA,gBAAgB,GAAG;GAClB,OAAOA,SAAC,CAAC,eAAe,EAAE,gCAAgC,CAAA;GAC1D;;EAED,sBAAsB,GAAG;AACxB,GAAA,OAAOA,SAAC,CAAC,eAAe,EAAE,kEAAkE,CAAA;GAC5F;;EAED,UAAU,GAAG;GACZ,OAAOA,SAAC,CAAC,eAAe,EAAE,4BAA4B,CAAA;GACtD;;EAED,UAAU,GAAG;AACZ,GAAA,OAAOA,SAAC,CAAC,eAAe,EAAE,sBAAsB,CAAA;GAChD;;AAED,EAAA,gBAAgB,GAAG;GAClB,OAAOA,SAAC,CAAC,eAAe,EAAE,+FAA+F,CAAA;GACzH;;EAED,UAAU,GAAG;AACZ,GAAA,OAAOA,SAAC,CAAC,eAAe,EAAE,OAAO,CAAA;GACjC;EACD;;AAED,CAAA,OAAO,GAAG;AACT,EAAA,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE;AACnB,GAAA,IAAI,CAAC,UAAU,GAAC;AACjB,GAAA;EACA;;AAED,CAAA,OAAO,EAAE;AACR,EAAA,MAAM,UAAU,GAAG;AAClB,GAAA,IAAI,CAAC,OAAM,GAAI,KAAG;AAClB,GAAA,IAAI,CAAC,QAAO,GAAI,KAAG;GACnB,IAAI,CAAC,KAAM,GAAE,GAAC;GACd,MAAM,EAAE,KAAK,EAAE,QAAO,KAAM,MAAM,kBAAkB,CAAC,IAAI,CAAC,IAAI,EAAA;AAC9D,GAAA,IAAI,CAAC,KAAM,GAAE,MAAI;AACjB,GAAA,IAAI,CAAC,QAAO,GAAI,SAAO;AACvB,GAAA,IAAI,CAAC,OAAQ,GAAE,MAAI;GACnB;EACD;AACF;;;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnRoadmapTab.vue3.js","sources":["../../../../src/components/CnRoadmapTab/CnRoadmapTab.vue?vue&type=template&id=
|
|
1
|
+
{"version":3,"file":"CnRoadmapTab.vue3.js","sources":["../../../../src/components/CnRoadmapTab/CnRoadmapTab.vue?vue&type=template&id=732ef80f&lang.js"],"sourcesContent":["<template>\n\t<div class=\"cn-roadmap-tab\">\n\t\t<div v-if=\"disabled\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"disabledLabel\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<InformationOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else-if=\"loading\" class=\"cn-roadmap-tab__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\t\t<div v-else-if=\"degraded === 'pat_not_configured'\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"patNotConfiguredTitle\" :description=\"patNotConfiguredDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<KeyOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else-if=\"degraded === 'rate_limited'\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"rateLimitedTitle\" :description=\"rateLimitedDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<ClockOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else-if=\"degraded === 'error'\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"errorTitle\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<AlertCircleOutline :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t\t<template #action>\n\t\t\t\t\t<NcButton variant=\"primary\" @click=\"fetchItems\">\n\t\t\t\t\t\t{{ retryLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else-if=\"sortedItems.length === 0\" class=\"cn-roadmap-tab__empty\">\n\t\t\t<NcEmptyContent :name=\"emptyTitle\" :description=\"emptyDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<RoadVariant :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\n\t\t<div v-else class=\"cn-roadmap-tab__grid\">\n\t\t\t<CnRoadmapItem\n\t\t\t\tv-for=\"item in sortedItems\"\n\t\t\t\t:key=\"item.number\"\n\t\t\t\t:item=\"item\"\n\t\t\t\tclass=\"cn-roadmap-tab__card\" />\n\t\t</div>\n\t</div>\n</template>\n\n<script>\n/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n *\n * RoadmapTab — reaction-sorted GitHub issues feed. Fetches from the\n * OpenRegister `github-issue-proxy` GET endpoint on mount with the curated\n * label filter (`labels=enhancement,feature`, per openregister design D23)\n * and renders each item via `RoadmapItem`.\n *\n * Handles four documented degraded states with distinct localized messages:\n * `hint: github_pat_not_configured`, HTTP 429 `github_rate_limited`, network\n * errors, and the empty-array edge case.\n *\n * Spec: features-roadmap-component — Requirement \"RoadmapTab\".\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'\nimport ClockOutline from 'vue-material-design-icons/ClockOutline.vue'\nimport InformationOutline from 'vue-material-design-icons/InformationOutline.vue'\nimport KeyOutline from 'vue-material-design-icons/KeyOutline.vue'\nimport RoadVariant from 'vue-material-design-icons/RoadVariant.vue'\nimport CnRoadmapItem from '../CnRoadmapItem/CnRoadmapItem.vue'\nimport { fetchRoadmapIssues, sortByReactions } from '../../utils/roadmapIssues.js'\n\nexport default {\n\tname: 'CnRoadmapTab',\n\n\tcomponents: {\n\t\tAlertCircleOutline,\n\t\tClockOutline,\n\t\tInformationOutline,\n\t\tKeyOutline,\n\t\tRoadVariant,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tNcButton,\n\t\tCnRoadmapItem,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * `<owner>/<repo>` of the app's GitHub repository. Passed through\n\t\t * to the proxy as the `repo` query parameter.\n\t\t */\n\t\trepo: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * Admin opt-out flag — when true the tab renders the disabled empty state.\n\t\t */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tloading: false,\n\t\t\titems: [],\n\t\t\tdegraded: null, // null | 'pat_not_configured' | 'rate_limited' | 'error'\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tsortedItems() {\n\t\t\treturn sortByReactions(this.items)\n\t\t},\n\n\t\tdisabledLabel() {\n\t\t\treturn t('nextcloud-vue', 'Roadmap is disabled by your administrator')\n\t\t},\n\n\t\tpatNotConfiguredTitle() {\n\t\t\treturn t('nextcloud-vue', 'Roadmap not yet configured')\n\t\t},\n\n\t\tpatNotConfiguredDescription() {\n\t\t\treturn t('nextcloud-vue', 'Ask your administrator to add a GitHub Personal Access Token in the OpenRegister settings to enable the roadmap.')\n\t\t},\n\n\t\trateLimitedTitle() {\n\t\t\treturn t('nextcloud-vue', 'GitHub temporarily unavailable')\n\t\t},\n\n\t\trateLimitedDescription() {\n\t\t\treturn t('nextcloud-vue', 'GitHub rate limited the request — please retry in a few minutes.')\n\t\t},\n\n\t\terrorTitle() {\n\t\t\treturn t('nextcloud-vue', 'Could not load the roadmap')\n\t\t},\n\n\t\temptyTitle() {\n\t\t\treturn t('nextcloud-vue', 'No roadmap items yet')\n\t\t},\n\n\t\temptyDescription() {\n\t\t\treturn t('nextcloud-vue', 'When the maintainers label issues \"enhancement\" or \"feature\" on GitHub they will appear here.')\n\t\t},\n\n\t\tretryLabel() {\n\t\t\treturn t('nextcloud-vue', 'Retry')\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (!this.disabled) {\n\t\t\tthis.fetchItems()\n\t\t}\n\t},\n\n\tmethods: {\n\t\tasync fetchItems() {\n\t\t\tthis.loading = true\n\t\t\tthis.degraded = null\n\t\t\tthis.items = []\n\t\t\tconst { items, degraded } = await fetchRoadmapIssues(this.repo)\n\t\t\tthis.items = items\n\t\t\tthis.degraded = degraded\n\t\t\tthis.loading = false\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-roadmap-tab__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n\tgap: 16px;\n}\n\n.cn-roadmap-tab__card {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\ttransition: border-color 120ms ease, box-shadow 120ms ease;\n}\n\n.cn-roadmap-tab__card:hover {\n\tborder-color: var(--color-primary-element);\n\tbox-shadow: 0 2px 8px var(--color-box-shadow, rgba(0, 0, 0, 0.08));\n}\n\n.cn-roadmap-tab__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 32px;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createVNode","_Fragment","_renderList","_createBlock"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import script from './CnUserSettingsDialog.vue2.js';
|
|
2
|
+
import { render } from './CnUserSettingsDialog.vue3.js';
|
|
3
|
+
|
|
4
|
+
script.render = render;
|
|
5
|
+
script.__scopeId = "data-v-8ebfd3c8";
|
|
6
|
+
const __sfc_main = script;
|
|
7
|
+
|
|
8
|
+
export { __sfc_main as default };
|
|
9
|
+
//# sourceMappingURL=CnUserSettingsDialog.vue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnUserSettingsDialog.vue.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { translate } from '@nextcloud/l10n';
|
|
2
|
+
import { NcButton } from '@nextcloud/vue';
|
|
3
|
+
import { markRaw } from 'vue';
|
|
4
|
+
import __sfc_main from '../CnDialog/CnDialog.vue.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The key NcAppSettingsSection injects to register itself with its dialog
|
|
8
|
+
* (`Symbol.for`, so the same symbol `@nextcloud/vue` provides).
|
|
9
|
+
*/
|
|
10
|
+
const REGISTRATION_KEY = Symbol.for('NcAppSettingsDialog:registration');
|
|
11
|
+
|
|
12
|
+
/** Renders a section's icon vnodes as they were registered. */
|
|
13
|
+
const CnVNodes = {
|
|
14
|
+
name: 'CnVNodes',
|
|
15
|
+
props: { vnodes: { type: [Array, Object], default: null } },
|
|
16
|
+
render() {
|
|
17
|
+
return this.vnodes
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* CnUserSettingsDialog - the user settings dialog in the board look (board
|
|
23
|
+
* PqPersoonlijk): eyebrow, title and subtitle, a left navigation listing the
|
|
24
|
+
* sections with their icons, the sections themselves, and a footer with a
|
|
25
|
+
* note that changes save straight away and a Close button.
|
|
26
|
+
*
|
|
27
|
+
* The sections stay the app's: any `NcAppSettingsSection` in the default slot
|
|
28
|
+
* registers itself with this dialog exactly as it does with
|
|
29
|
+
* `NcAppSettingsDialog`, with its name, order and `#icon` slot. CnAppRoot
|
|
30
|
+
* renders this dialog instead of `NcAppSettingsDialog` under the board look.
|
|
31
|
+
*
|
|
32
|
+
* @spec openspec/changes/user-settings-board-dialog/specs/user-settings-board-dialog/spec.md#requirement-the-user-settings-dialog-takes-the-board-look
|
|
33
|
+
*
|
|
34
|
+
* @event update:open Emitted with `false` when the dialog closes.
|
|
35
|
+
*/
|
|
36
|
+
var script = {
|
|
37
|
+
name: 'CnUserSettingsDialog',
|
|
38
|
+
|
|
39
|
+
components: { CnDialog: __sfc_main, CnVNodes, NcButton },
|
|
40
|
+
|
|
41
|
+
provide() {
|
|
42
|
+
return {
|
|
43
|
+
[REGISTRATION_KEY]: {
|
|
44
|
+
registerSection: this.registerSection,
|
|
45
|
+
unregisterSection: this.unregisterSection,
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
props: {
|
|
51
|
+
/** Whether the dialog is open. */
|
|
52
|
+
open: { type: Boolean, default: false },
|
|
53
|
+
/** Dialog title; also its accessible name. */
|
|
54
|
+
title: { type: String, required: true },
|
|
55
|
+
/** Context line above the title. */
|
|
56
|
+
eyebrow: { type: String, default: () => translate('nextcloud-vue', 'Your account') },
|
|
57
|
+
/** Sentence under the title. */
|
|
58
|
+
subtitle: { type: String, default: () => translate('nextcloud-vue', 'Only for you. Colleagues keep their own settings.') },
|
|
59
|
+
/** The app's name, after the footer note. */
|
|
60
|
+
appName: { type: String, default: '' },
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
emits: ['update:open'],
|
|
64
|
+
|
|
65
|
+
data() {
|
|
66
|
+
return {
|
|
67
|
+
sections: [],
|
|
68
|
+
selected: '',
|
|
69
|
+
picked: false,
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
computed: {
|
|
74
|
+
/** @return {object} The board's eyebrow: muted, in sentence case. */
|
|
75
|
+
eyebrowStyle() {
|
|
76
|
+
return { '--cn-dialog-eyebrow-transform': 'none', '--cn-dialog-eyebrow-color': 'var(--color-text-maxcontrast)' }
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/** @return {string} The navigation's accessible name. */
|
|
80
|
+
navLabel() {
|
|
81
|
+
return translate('nextcloud-vue', 'Sections of the settings')
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/** @return {string} The footer note, with the app's name when known. */
|
|
85
|
+
footerNote() {
|
|
86
|
+
const note = translate('nextcloud-vue', 'Changes are saved straight away');
|
|
87
|
+
return this.appName ? `${note} · ${this.appName}` : note
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/** @return {string} The Close button label. */
|
|
91
|
+
closeLabel() {
|
|
92
|
+
return translate('nextcloud-vue', 'Close')
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
methods: {
|
|
97
|
+
/**
|
|
98
|
+
* Add a section to the navigation, kept in `order`; a section
|
|
99
|
+
* without one goes last. Until the reader picks one, the first
|
|
100
|
+
* section is current.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} id The section id.
|
|
103
|
+
* @param {string} name The section name.
|
|
104
|
+
* @param {number} [order] The section's place.
|
|
105
|
+
* @param {Array} [icon] The section's icon vnodes.
|
|
106
|
+
*/
|
|
107
|
+
registerSection(id, name, order, icon) {
|
|
108
|
+
const rest = this.sections.filter((section) => section.id !== id);
|
|
109
|
+
const place = typeof order === 'number' ? order : Math.max(0, ...rest.map((section) => section.order)) + 1;
|
|
110
|
+
this.sections = [...rest, { id, name, order: place, icon: icon ? markRaw(icon) : null }].sort((a, b) => a.order - b.order);
|
|
111
|
+
if (!this.picked) {
|
|
112
|
+
this.selected = this.sections[0].id;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Remove a section from the navigation.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} id The section id.
|
|
120
|
+
*/
|
|
121
|
+
unregisterSection(id) {
|
|
122
|
+
this.sections = this.sections.filter((section) => section.id !== id);
|
|
123
|
+
if (this.selected === id) {
|
|
124
|
+
this.selected = this.sections.length > 0 ? this.sections[0].id : '';
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Mark a section current and scroll it into view.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} id The section id.
|
|
132
|
+
*/
|
|
133
|
+
selectSection(id) {
|
|
134
|
+
this.picked = true;
|
|
135
|
+
this.selected = id;
|
|
136
|
+
const target = typeof document !== 'undefined' ? document.getElementById(`settings-section_${id}`) : null;
|
|
137
|
+
if (target && typeof target.scrollIntoView === 'function') {
|
|
138
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Follow NcDialog's own open state.
|
|
144
|
+
*
|
|
145
|
+
* @param {boolean} value Whether NcDialog is open.
|
|
146
|
+
*/
|
|
147
|
+
onUpdateOpen(value) {
|
|
148
|
+
if (!value) {
|
|
149
|
+
this.close();
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/** Close the dialog. */
|
|
154
|
+
close() {
|
|
155
|
+
/**
|
|
156
|
+
* @event update:open Emitted with `false` when the dialog closes (Close button, close icon, Escape).
|
|
157
|
+
*/
|
|
158
|
+
this.$emit('update:open', false);
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export { script as default };
|
|
164
|
+
//# sourceMappingURL=CnUserSettingsDialog.vue2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnUserSettingsDialog.vue2.js","sources":["../../../../src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<CnDialog\n\t\tclass=\"cn-user-settings-dialog\"\n\t\t:name=\"title\"\n\t\tlook=\"board\"\n\t\twidth=\"wizard\"\n\t\t:eyebrow=\"eyebrow\"\n\t\t:subtitle=\"subtitle\"\n\t\t:open=\"open\"\n\t\t:style=\"eyebrowStyle\"\n\t\tdata-testid=\"cn-user-settings-dialog\"\n\t\t@update:open=\"onUpdateOpen\"\n\t\t@closing=\"close\">\n\t\t<div class=\"cn-user-settings-dialog__body\">\n\t\t\t<nav\n\t\t\t\tv-if=\"sections.length > 0\"\n\t\t\t\tclass=\"cn-user-settings-dialog__nav\"\n\t\t\t\t:aria-label=\"navLabel\"\n\t\t\t\tdata-testid=\"cn-user-settings-nav\">\n\t\t\t\t<ul>\n\t\t\t\t\t<li v-for=\"section in sections\" :key=\"section.id\">\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t:href=\"`#settings-section_${section.id}`\"\n\t\t\t\t\t\t\tclass=\"cn-user-settings-dialog__nav-link\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-user-settings-dialog__nav-link--active': section.id === selected }\"\n\t\t\t\t\t\t\t:aria-current=\"section.id === selected ? 'true' : undefined\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-user-settings-nav-${section.id}`\"\n\t\t\t\t\t\t\t@click.prevent=\"selectSection(section.id)\">\n\t\t\t\t\t\t\t<span v-if=\"section.icon\" class=\"cn-user-settings-dialog__nav-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t<CnVNodes :vnodes=\"section.icon\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span>{{ section.name }}</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</nav>\n\t\t\t<div class=\"cn-user-settings-dialog__sections\">\n\t\t\t\t<!-- @slot default NcAppSettingsSection children; each registers itself in the navigation. -->\n\t\t\t\t<slot />\n\t\t\t</div>\n\t\t</div>\n\t\t<template #actions>\n\t\t\t<p class=\"cn-user-settings-dialog__note\" data-testid=\"cn-user-settings-note\">\n\t\t\t\t{{ footerNote }}\n\t\t\t</p>\n\t\t\t<NcButton variant=\"secondary\" data-testid=\"cn-user-settings-close\" @click=\"close\">\n\t\t\t\t{{ closeLabel }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</CnDialog>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport CnDialog from '../CnDialog/CnDialog.vue'\n\n/**\n * The key NcAppSettingsSection injects to register itself with its dialog\n * (`Symbol.for`, so the same symbol `@nextcloud/vue` provides).\n */\nconst REGISTRATION_KEY = Symbol.for('NcAppSettingsDialog:registration')\n\n/** Renders a section's icon vnodes as they were registered. */\nconst CnVNodes = {\n\tname: 'CnVNodes',\n\tprops: { vnodes: { type: [Array, Object], default: null } },\n\trender() {\n\t\treturn this.vnodes\n\t},\n}\n\n/**\n * CnUserSettingsDialog - the user settings dialog in the board look (board\n * PqPersoonlijk): eyebrow, title and subtitle, a left navigation listing the\n * sections with their icons, the sections themselves, and a footer with a\n * note that changes save straight away and a Close button.\n *\n * The sections stay the app's: any `NcAppSettingsSection` in the default slot\n * registers itself with this dialog exactly as it does with\n * `NcAppSettingsDialog`, with its name, order and `#icon` slot. CnAppRoot\n * renders this dialog instead of `NcAppSettingsDialog` under the board look.\n *\n * @spec openspec/changes/user-settings-board-dialog/specs/user-settings-board-dialog/spec.md#requirement-the-user-settings-dialog-takes-the-board-look\n *\n * @event update:open Emitted with `false` when the dialog closes.\n */\nexport default {\n\tname: 'CnUserSettingsDialog',\n\n\tcomponents: { CnDialog, CnVNodes, NcButton },\n\n\tprovide() {\n\t\treturn {\n\t\t\t[REGISTRATION_KEY]: {\n\t\t\t\tregisterSection: this.registerSection,\n\t\t\t\tunregisterSection: this.unregisterSection,\n\t\t\t},\n\t\t}\n\t},\n\n\tprops: {\n\t\t/** Whether the dialog is open. */\n\t\topen: { type: Boolean, default: false },\n\t\t/** Dialog title; also its accessible name. */\n\t\ttitle: { type: String, required: true },\n\t\t/** Context line above the title. */\n\t\teyebrow: { type: String, default: () => t('nextcloud-vue', 'Your account') },\n\t\t/** Sentence under the title. */\n\t\tsubtitle: { type: String, default: () => t('nextcloud-vue', 'Only for you. Colleagues keep their own settings.') },\n\t\t/** The app's name, after the footer note. */\n\t\tappName: { type: String, default: '' },\n\t},\n\n\temits: ['update:open'],\n\n\tdata() {\n\t\treturn {\n\t\t\tsections: [],\n\t\t\tselected: '',\n\t\t\tpicked: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object} The board's eyebrow: muted, in sentence case. */\n\t\teyebrowStyle() {\n\t\t\treturn { '--cn-dialog-eyebrow-transform': 'none', '--cn-dialog-eyebrow-color': 'var(--color-text-maxcontrast)' }\n\t\t},\n\n\t\t/** @return {string} The navigation's accessible name. */\n\t\tnavLabel() {\n\t\t\treturn t('nextcloud-vue', 'Sections of the settings')\n\t\t},\n\n\t\t/** @return {string} The footer note, with the app's name when known. */\n\t\tfooterNote() {\n\t\t\tconst note = t('nextcloud-vue', 'Changes are saved straight away')\n\t\t\treturn this.appName ? `${note} · ${this.appName}` : note\n\t\t},\n\n\t\t/** @return {string} The Close button label. */\n\t\tcloseLabel() {\n\t\t\treturn t('nextcloud-vue', 'Close')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Add a section to the navigation, kept in `order`; a section\n\t\t * without one goes last. Until the reader picks one, the first\n\t\t * section is current.\n\t\t *\n\t\t * @param {string} id The section id.\n\t\t * @param {string} name The section name.\n\t\t * @param {number} [order] The section's place.\n\t\t * @param {Array} [icon] The section's icon vnodes.\n\t\t */\n\t\tregisterSection(id, name, order, icon) {\n\t\t\tconst rest = this.sections.filter((section) => section.id !== id)\n\t\t\tconst place = typeof order === 'number' ? order : Math.max(0, ...rest.map((section) => section.order)) + 1\n\t\t\tthis.sections = [...rest, { id, name, order: place, icon: icon ? markRaw(icon) : null }].sort((a, b) => a.order - b.order)\n\t\t\tif (!this.picked) {\n\t\t\t\tthis.selected = this.sections[0].id\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove a section from the navigation.\n\t\t *\n\t\t * @param {string} id The section id.\n\t\t */\n\t\tunregisterSection(id) {\n\t\t\tthis.sections = this.sections.filter((section) => section.id !== id)\n\t\t\tif (this.selected === id) {\n\t\t\t\tthis.selected = this.sections.length > 0 ? this.sections[0].id : ''\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Mark a section current and scroll it into view.\n\t\t *\n\t\t * @param {string} id The section id.\n\t\t */\n\t\tselectSection(id) {\n\t\t\tthis.picked = true\n\t\t\tthis.selected = id\n\t\t\tconst target = typeof document !== 'undefined' ? document.getElementById(`settings-section_${id}`) : null\n\t\t\tif (target && typeof target.scrollIntoView === 'function') {\n\t\t\t\ttarget.scrollIntoView({ behavior: 'smooth', block: 'start' })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Follow NcDialog's own open state.\n\t\t *\n\t\t * @param {boolean} value Whether NcDialog is open.\n\t\t */\n\t\tonUpdateOpen(value) {\n\t\t\tif (!value) {\n\t\t\t\tthis.close()\n\t\t\t}\n\t\t},\n\n\t\t/** Close the dialog. */\n\t\tclose() {\n\t\t\t/**\n\t\t\t * @event update:open Emitted with `false` when the dialog closes (Close button, close icon, Escape).\n\t\t\t */\n\t\t\tthis.$emit('update:open', false)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-user-settings-dialog__body {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 0 24px;\n}\n\n.cn-user-settings-dialog__nav {\n\tflex: 0 0 200px;\n\twidth: 200px;\n\tpadding-top: 18px;\n}\n\n.cn-user-settings-dialog__nav ul {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-user-settings-dialog__nav-link {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tmin-height: 40px;\n\tpadding: 0 12px;\n\tborder-radius: var(--border-radius-element, 8px);\n\tfont-size: 15px;\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n}\n\n.cn-user-settings-dialog__nav-link:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-user-settings-dialog__nav-link:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-user-settings-dialog__nav-link--active,\n.cn-user-settings-dialog__nav-link--active:hover {\n\tbackground: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-light-text);\n\tfont-weight: 600;\n}\n\n.cn-user-settings-dialog__nav-icon {\n\tdisplay: inline-flex;\n\tflex: none;\n\twidth: 18px;\n\theight: 18px;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n.cn-user-settings-dialog__sections {\n\tdisplay: flex;\n\tflex: 1 1 380px;\n\tflex-direction: column;\n\tmin-width: 0;\n}\n\n.cn-user-settings-dialog__sections :deep(.app-settings-section) {\n\tmargin: 0;\n\tpadding: 18px 0;\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-user-settings-dialog__sections :deep(.app-settings-section__name) {\n\tmargin: 0 0 12px;\n\tfont-size: 17px;\n\tfont-weight: 700;\n}\n\n.cn-user-settings-dialog__note {\n\tflex: 1 1 auto;\n\tmargin: 0;\n\tfont-size: 14px;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["CnDialog","t"],"mappings":"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"}
|