@conduction/nextcloud-vue 2.56.0-beta.21 → 2.56.0-beta.23

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.
Files changed (66) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +29 -0
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  4. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +18 -0
  5. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  6. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +9 -6
  7. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  8. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +16 -1
  9. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  10. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +5 -4
  11. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  12. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue2.js +25 -0
  13. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue2.js.map +1 -1
  14. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue3.js +3 -1
  15. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue3.js.map +1 -1
  16. package/dist/esm/components/CnGraphCanvas/CnFlowNode.vue2.js +36 -1
  17. package/dist/esm/components/CnGraphCanvas/CnFlowNode.vue2.js.map +1 -1
  18. package/dist/esm/components/CnGraphCanvas/CnFlowNode.vue3.js.map +1 -1
  19. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue2.js +37 -0
  20. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue2.js.map +1 -1
  21. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue3.js +4 -2
  22. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue3.js.map +1 -1
  23. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +16 -1
  24. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  25. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +6 -6
  26. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +53 -5
  28. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
  29. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +1 -1
  30. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
  31. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +133 -0
  32. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
  33. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js +22 -14
  34. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
  35. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +15 -1
  36. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  37. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +3 -2
  38. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  39. package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue2.js +15 -1
  40. package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue2.js.map +1 -1
  41. package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue3.js +2 -2
  42. package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue3.js.map +1 -1
  43. package/dist/esm/index.js +1 -0
  44. package/dist/esm/index.js.map +1 -1
  45. package/dist/esm/utils/notificationRuleLabel.js +132 -0
  46. package/dist/esm/utils/notificationRuleLabel.js.map +1 -0
  47. package/dist/nextcloud-vue.cjs.js +578 -47
  48. package/dist/nextcloud-vue.cjs.js.map +2 -2
  49. package/package.json +1 -1
  50. package/scripts/openregister-reliance.js +206 -0
  51. package/src/components/CnAppRoot/CnAppRoot.md +1 -0
  52. package/src/components/CnAppRoot/CnAppRoot.vue +29 -0
  53. package/src/components/CnDetailPage/CnDetailPage.vue +24 -3
  54. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +22 -4
  55. package/src/components/CnFlowDetail/CnFlowDetail.vue +27 -0
  56. package/src/components/CnGraphCanvas/CnFlowNode.vue +36 -1
  57. package/src/components/CnGraphCanvas/CnGraphCanvas.vue +40 -1
  58. package/src/components/CnIndexPage/CnIndexPage.vue +17 -2
  59. package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +54 -6
  60. package/src/components/CnPageRenderer/CnPageRenderer.vue +147 -6
  61. package/src/components/CnTabsWidget/CnTabsWidget.vue +17 -2
  62. package/src/components/CnWidgetGrid/CnWidgetGrid.vue +19 -3
  63. package/src/index.js +2 -0
  64. package/src/utils/notificationRuleLabel.js +129 -0
  65. package/webpack/index.d.ts +16 -0
  66. package/webpack/index.js +70 -0
@@ -1,9 +1,10 @@
1
1
  import axios__default from '@nextcloud/axios';
2
- import { translate } from '@nextcloud/l10n';
2
+ import { translate, getLanguage } from '@nextcloud/l10n';
3
3
  import { generateUrl } from '@nextcloud/router';
4
4
  import { NcAppSettingsSection, NcButton, NcCheckboxRadioSwitch, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue';
5
5
  import BellOffOutline from 'vue-material-design-icons/BellOffOutline.vue';
6
6
  import BellOutline from 'vue-material-design-icons/BellOutline.vue';
7
+ import { notificationRuleLabel } from '../../utils/notificationRuleLabel.js';
7
8
 
8
9
  const PREFS_PATH = '/apps/openregister/api/notification-preferences';
9
10
 
@@ -28,6 +29,28 @@ var script = {
28
29
  * than an always-empty pane.
29
30
  */
30
31
  cnAppId: { default: () => '' },
32
+ /**
33
+ * App-supplied labels for its notification rules, provided by the
34
+ * CnAppRoot ancestor from its `notificationLabels` prop. Keyed
35
+ * `<schema>.<key>` or `<key>`.
36
+ */
37
+ cnNotificationLabels: { default: () => ({}) },
38
+ },
39
+
40
+ props: {
41
+ /**
42
+ * Labels for the notification rules, keyed `<schema>.<key>` or
43
+ * `<key>`; values are strings or per-locale maps (`{ nl, en }`).
44
+ * Optional: inside CnAppRoot the app's `notificationLabels` arrive by
45
+ * inject, and a rule without a label still reads as words, never as
46
+ * its raw key. A prop given here wins over the injected map.
47
+ *
48
+ * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md
49
+ */
50
+ labels: {
51
+ type: Object,
52
+ default: null,
53
+ },
31
54
  },
32
55
 
33
56
  data() {
@@ -153,18 +176,43 @@ var script = {
153
176
  },
154
177
 
155
178
  /**
156
- * Human-readable label for a notification key, falling back to the key.
179
+ * Human-readable label for a notification rule. Never the raw key: the
180
+ * rule's own label, then the app's, then the library's wording for the
181
+ * generic keys, then the key split into words.
157
182
  *
158
- * @param {string} key The notification annotation key.
183
+ * @param {object} entry The preference row.
159
184
  * @return {string} A display label.
185
+ *
186
+ * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md
160
187
  */
161
- notificationLabel(key) {
188
+ notificationLabel(entry) {
162
189
  const known = {
163
190
  object_created: translate('nextcloud-vue', 'When an item is created'),
164
191
  object_updated: translate('nextcloud-vue', 'When an item is updated'),
165
192
  object_transitioned: translate('nextcloud-vue', 'When an item is assigned or changes status'),
166
193
  };
167
- return known[key] || key
194
+ const language = this.currentLanguage();
195
+ const injected = typeof this.cnNotificationLabels === 'function'
196
+ ? this.cnNotificationLabels()
197
+ : this.cnNotificationLabels;
198
+ return notificationRuleLabel(entry, {
199
+ labels: { ...(injected || {}), ...(this.labels || {}) },
200
+ known,
201
+ language,
202
+ })
203
+ },
204
+
205
+ /**
206
+ * The person's language, `en` when it cannot be read.
207
+ *
208
+ * @return {string} The language code.
209
+ */
210
+ currentLanguage() {
211
+ try {
212
+ return (typeof getLanguage === 'function' && getLanguage()) || 'en'
213
+ } catch {
214
+ return 'en'
215
+ }
168
216
  },
169
217
 
170
218
  /**
@@ -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.notification) }}\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 { 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'\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},\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 key, falling back to the key.\n\t\t *\n\t\t * @param {string} key The notification annotation key.\n\t\t * @return {string} A display label.\n\t\t */\n\t\tnotificationLabel(key) {\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\treturn known[key] || key\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<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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"}
@@ -75,7 +75,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
75
75
  "onUpdate:modelValue": $event => ($options.onToggle(entry, $event))
76
76
  }, {
77
77
  default: withCtx(() => [
78
- createTextVNode(toDisplayString($options.notificationLabel(entry.notification)), 1 /* TEXT */)
78
+ createTextVNode(toDisplayString($options.notificationLabel(entry)), 1 /* TEXT */)
79
79
  ]),
80
80
  _: 2 /* DYNAMIC */
81
81
  }, 1032 /* PROPS, DYNAMIC_SLOTS */, ["modelValue", "disabled", "onUpdate:modelValue"]),
@@ -1 +1 @@
1
- {"version":3,"file":"CnNotificationPreferences.vue3.js","sources":["../../../../src/components/CnNotificationPreferences/CnNotificationPreferences.vue?vue&type=template&id=72cc0056&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.notification) }}\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 { 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'\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},\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 key, falling back to the key.\n\t\t *\n\t\t * @param {string} key The notification annotation key.\n\t\t * @return {string} A display label.\n\t\t */\n\t\tnotificationLabel(key) {\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\treturn known[key] || key\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"],"mappings":"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"}
1
+ {"version":3,"file":"CnNotificationPreferences.vue3.js","sources":["../../../../src/components/CnNotificationPreferences/CnNotificationPreferences.vue?vue&type=template&id=72cc0056&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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"}
@@ -21,6 +21,53 @@ import __sfc_main$4 from '../CnMassExportDialog/CnMassExportDialog.vue.js';
21
21
  import { defaultPageTypes } from './pageTypes.js';
22
22
  import { useObjectStore } from '../../store/useObjectStore.js';
23
23
 
24
+ /**
25
+ * OpenRegister registers and schemas already described by id, so a click on
26
+ * a related object costs at most one request per register or schema for as
27
+ * long as this module stays loaded (the SPA's lifetime, across page
28
+ * changes). Keyed by `${kind}:${id}`; a failed lookup is dropped again so the
29
+ * next click retries it.
30
+ *
31
+ * @type {Map<string, Promise<{ id: string, slug: string }>>}
32
+ */
33
+ const entityDescriptorCache = new Map();
34
+
35
+ /**
36
+ * Describe an OpenRegister register or schema (`{ id, slug }`) by its id or
37
+ * slug, or null when it cannot be fetched. An object's `@self` names its
38
+ * register and schema by id while a manifest may name them by slug; this is
39
+ * the bridge. Lazy-imports axios and the router helper like the widgets do.
40
+ *
41
+ * @param {'registers'|'schemas'} kind The collection to ask.
42
+ * @param {string|number} idOrSlug The id (or slug) to describe.
43
+ * @return {Promise<{ id: string, slug: string }|null>} The descriptor.
44
+ */
45
+ function describeOpenRegisterEntity(kind, idOrSlug) {
46
+ const key = `${kind}:${idOrSlug}`;
47
+ if (!entityDescriptorCache.has(key)) {
48
+ entityDescriptorCache.set(key, (async () => {
49
+ try {
50
+ const [{ default: axios }, { generateUrl }] = await Promise.all([
51
+ import('@nextcloud/axios'),
52
+ import('@nextcloud/router'),
53
+ ]);
54
+ const url = generateUrl('/apps/openregister/api/{kind}/{id}', { kind, id: String(idOrSlug) });
55
+ const res = await axios.get(url);
56
+ const data = res && res.data;
57
+ if (!data || typeof data !== 'object') {
58
+ throw new Error(`no descriptor for ${key}`)
59
+ }
60
+ return { id: String(data.id ?? idOrSlug), slug: String(data.slug ?? '') }
61
+ } catch (error) {
62
+ // One failed request must not pin every later click to nowhere.
63
+ entityDescriptorCache.delete(key);
64
+ throw error
65
+ }
66
+ })());
67
+ }
68
+ return entityDescriptorCache.get(key).catch(() => null)
69
+ }
70
+
24
71
  /** Recognised fixed slot names for v2 manifests. */
25
72
  const KNOWN_SLOTS = new Set(['body', 'sidebar', 'header-actions', 'footer', 'modal']);
26
73
 
@@ -1548,6 +1595,92 @@ var script = {
1548
1595
  })
1549
1596
  },
1550
1597
 
1598
+ /**
1599
+ * Open the detail page of an object clicked in a Related widget —
1600
+ * reaching here as `CnDetailPage`'s `related-object-click`, or as
1601
+ * `CnWidgetGrid`'s `select-object` on a v2 widget-grid page. Nothing
1602
+ * listened to either, so the rows looked like links and went nowhere.
1603
+ *
1604
+ * @param {object} raw The clicked object as the Related widget holds it.
1605
+ * @return {Promise<void>}
1606
+ */
1607
+ async onRelatedObjectOpen(raw) {
1608
+ const target = await this.relatedObjectTarget(raw);
1609
+ if (target) {
1610
+ this.$router.push(target).catch(() => {});
1611
+ }
1612
+ },
1613
+
1614
+ /**
1615
+ * Where a related object opens: the first `type: 'detail'` page bound
1616
+ * to the object's register and schema, with the id in whichever param
1617
+ * that page's route declares. The object's `@self` names register and
1618
+ * schema by id and a manifest may name them by slug, so a miss on the
1619
+ * literal pair describes both (one request each, cached) and matches
1620
+ * again. Null, silently, when there is no router or the object carries
1621
+ * no register, schema or id (nothing to open); null with a warning,
1622
+ * like a dead row click, when no detail page matches or the router
1623
+ * lacks the route.
1624
+ *
1625
+ * @param {object} raw The clicked object.
1626
+ * @return {Promise<object|null>} The router location, or null.
1627
+ */
1628
+ async relatedObjectTarget(raw) {
1629
+ const router = this.$router;
1630
+ if (!router || !raw || typeof raw !== 'object') {
1631
+ return null
1632
+ }
1633
+ const self = raw['@self'] || {};
1634
+ const id = raw.id ?? self.id ?? self.uuid ?? raw.uuid;
1635
+ const register = self.register ?? raw.register;
1636
+ const schema = self.schema ?? raw.schema;
1637
+ const missing = (v) => v === undefined || v === null || v === '';
1638
+ if (missing(id) || missing(register) || missing(schema)) {
1639
+ return null
1640
+ }
1641
+ let page = this.detailPageFor([String(register)], [String(schema)]);
1642
+ if (!page) {
1643
+ const [reg, sch] = await Promise.all([
1644
+ describeOpenRegisterEntity('registers', register),
1645
+ describeOpenRegisterEntity('schemas', schema),
1646
+ ]);
1647
+ page = this.detailPageFor(
1648
+ [String(register), reg?.id, reg?.slug].filter(Boolean),
1649
+ [String(schema), sch?.id, sch?.slug].filter(Boolean),
1650
+ );
1651
+ }
1652
+ if (!page) {
1653
+ // eslint-disable-next-line no-console
1654
+ console.warn(`[CnPageRenderer] No detail page is bound to register "${register}" and schema "${schema}", so related object "${id}" opens nowhere.`);
1655
+ return null
1656
+ }
1657
+ if (this.routeNameIsKnown(page.id) === false) {
1658
+ // eslint-disable-next-line no-console
1659
+ console.warn(`[CnPageRenderer] Detail page "${page.id}" is not a route the router has, so related object "${id}" opens nowhere.`);
1660
+ return null
1661
+ }
1662
+ const path = page.route ?? routePathFor(router, page.id);
1663
+ return { name: page.id, params: buildRouteParams(path, id, this.$route?.params) }
1664
+ },
1665
+
1666
+ /**
1667
+ * The first `type: 'detail'` page whose register and schema are both
1668
+ * among the given names (ids or slugs, compared as strings).
1669
+ *
1670
+ * @param {Array<string>} registerNames Names the register goes by.
1671
+ * @param {Array<string>} schemaNames Names the schema goes by.
1672
+ * @return {object|null} The page, or null.
1673
+ */
1674
+ detailPageFor(registerNames, schemaNames) {
1675
+ for (const page of this.detailPageByRegisterSchema.values()) {
1676
+ const cfg = page.config || {};
1677
+ if (registerNames.includes(String(cfg.register)) && schemaNames.includes(String(cfg.schema))) {
1678
+ return page
1679
+ }
1680
+ }
1681
+ return null
1682
+ },
1683
+
1551
1684
  /**
1552
1685
  * Whether the router has a route by this name.
1553
1686
  *