@conduction/nextcloud-vue 2.73.0 → 2.73.1
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 +29 -0
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +18 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +9 -6
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +16 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +5 -4
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +16 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +6 -6
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +53 -5
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +133 -0
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js +22 -14
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +15 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +3 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue2.js +15 -1
- package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue3.js +2 -2
- package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue3.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/utils/notificationRuleLabel.js +132 -0
- package/dist/esm/utils/notificationRuleLabel.js.map +1 -0
- package/dist/nextcloud-vue.cjs.js +473 -43
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/package.json +1 -1
- package/src/components/CnAppRoot/CnAppRoot.md +1 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +29 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +24 -3
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +22 -4
- package/src/components/CnIndexPage/CnIndexPage.vue +17 -2
- package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +54 -6
- package/src/components/CnPageRenderer/CnPageRenderer.vue +147 -6
- package/src/components/CnTabsWidget/CnTabsWidget.vue +17 -2
- package/src/components/CnWidgetGrid/CnWidgetGrid.vue +19 -3
- package/src/index.js +2 -0
- package/src/utils/notificationRuleLabel.js +129 -0
- package/webpack/index.d.ts +16 -0
- package/webpack/index.js +70 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/dist/nextcloud-vue.cjs.js.map
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 35731122 bytes
|
|
4
|
+
sha256: 2f00e1767fdb1c72a109ba42b159ca43fb36fbd6a93a33087f4c8529da77b80b
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.73.
|
|
3
|
+
"version": "2.73.1",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -78,6 +78,7 @@ With loading state, custom components, translate, permissions, and custom page t
|
|
|
78
78
|
| `cellWidgets` | Object | No | `{}` | Cell-widget registry — map of widget-id → Vue component, rendered for a column that declares `pages[].config.columns[].widget`. The component receives `{ value, row, property, formatted, ...widgetProps }`. The library ships one built-in id, `"badge"` (renders `CnStatusBadge`); consumer entries cover everything else (status pills with custom colour maps, inline toggles, link cells, …). Provided to descendant `CnDataTable` / `CnCellRenderer` as `cnCellWidgets`. See `docs/migrating-to-manifest.md` → "Column widgets". |
|
|
79
79
|
| `kbSearchProviders` | Object | No | `{}` | Pluggable knowledge-base search providers (#91 Wave 3) — map of provider-key → `{ search(query, opts), externalOpen? }`, merged OVER the library built-in `default` (endpoint) provider and provided to descendant `CnKbSearchWidget` as `cnKbSearchProviders`. A `kb-search` widget selects its provider via `content.provider`; an app talking to a bespoke KB backend (the xwiki proxy) registers its client here — the library ships only the seam. |
|
|
80
80
|
| `translate` | Function | No | `key => key` | Translate function from the consuming app, typically `(key) => t(appId, key)`. Provided to descendants as `cnTranslate` |
|
|
81
|
+
| `notificationLabels` | Object | No | `{}` | Labels for the app's notification rules in the user-settings notification pane, keyed `<schema>.<key>` or `<key>`; values are translated strings or `{ nl, en }` maps. A rule without a label reads as its key split into words ("Case assigned"), never as the raw key. Provided to descendants as `cnNotificationLabels` |
|
|
81
82
|
| `permissions` | Array | No | `[]` | Permission strings for the current user; forwarded to `CnAppNav` for menu filtering |
|
|
82
83
|
| `pageTypes` | Object | No | `null` | Page-type registry map (`type → component`). Falls back to the library's `defaultPageTypes`. Extend with `{ ...defaultPageTypes, report: MyReportPage }` |
|
|
83
84
|
| `userSettingsTitle` | String | No | `''` | Title shown at the top of the hosted `NcAppSettingsDialog`. Empty (the default) resolves to `translate('User settings')`. Override per app to brand the modal (e.g. `'Decidiq preferences'`). |
|
|
@@ -996,6 +996,18 @@ export default {
|
|
|
996
996
|
* wiring.
|
|
997
997
|
*/
|
|
998
998
|
cnAppId: this.appId,
|
|
999
|
+
/**
|
|
1000
|
+
* The app's labels for its notification rules, read by the
|
|
1001
|
+
* notification-preferences pane in the user-settings modal. A
|
|
1002
|
+
* getter so a late prop update is still seen by a pane that
|
|
1003
|
+
* mounts after it.
|
|
1004
|
+
*
|
|
1005
|
+
* @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md
|
|
1006
|
+
*/
|
|
1007
|
+
get cnNotificationLabels() {
|
|
1008
|
+
return self.notificationLabels || {}
|
|
1009
|
+
},
|
|
1010
|
+
|
|
999
1011
|
/**
|
|
1000
1012
|
* Target repo slug for the in-product feature-request deep
|
|
1001
1013
|
* link (e.g. `ConductionNL/pipelinq`). Read from the
|
|
@@ -1411,6 +1423,23 @@ export default {
|
|
|
1411
1423
|
default: (key) => key,
|
|
1412
1424
|
},
|
|
1413
1425
|
|
|
1426
|
+
/**
|
|
1427
|
+
* Labels for the app's notification rules, shown in the
|
|
1428
|
+
* user-settings notification pane instead of the rule keys. Keyed
|
|
1429
|
+
* `<schema>.<key>` (or just `<key>`); values are already translated
|
|
1430
|
+
* strings, e.g. `{ 'case.caseAssigned': t('dossiq', 'A case is
|
|
1431
|
+
* assigned to me') }`, or per-locale maps `{ nl, en }`. A rule's own
|
|
1432
|
+
* `label` from OpenRegister wins; a rule with no label anywhere reads
|
|
1433
|
+
* as its key split into words, never as the raw key.
|
|
1434
|
+
*
|
|
1435
|
+
* @type {{[key: string]: (string|object)}}
|
|
1436
|
+
* @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md
|
|
1437
|
+
*/
|
|
1438
|
+
notificationLabels: {
|
|
1439
|
+
type: Object,
|
|
1440
|
+
default: () => ({}),
|
|
1441
|
+
},
|
|
1442
|
+
|
|
1414
1443
|
/**
|
|
1415
1444
|
* List of permission strings the current user holds. Forwarded
|
|
1416
1445
|
* to CnAppNav's permission filter.
|
|
@@ -454,7 +454,8 @@
|
|
|
454
454
|
:cnRegistry="cnRegistry"
|
|
455
455
|
:availableWidgets="widgets"
|
|
456
456
|
@geoSaved="onGeoSaved"
|
|
457
|
-
@openIntegration="onAutoBodyOpenIntegration"
|
|
457
|
+
@openIntegration="onAutoBodyOpenIntegration"
|
|
458
|
+
@selectObject="onRelatedObjectSelect" />
|
|
458
459
|
</slot>
|
|
459
460
|
</div>
|
|
460
461
|
</div>
|
|
@@ -764,7 +765,8 @@
|
|
|
764
765
|
:cnRegistry="cnRegistry"
|
|
765
766
|
:availableWidgets="bodyGridWidgets"
|
|
766
767
|
@geoSaved="onGeoSaved"
|
|
767
|
-
@openIntegration="onAutoBodyOpenIntegration"
|
|
768
|
+
@openIntegration="onAutoBodyOpenIntegration"
|
|
769
|
+
@selectObject="onRelatedObjectSelect" />
|
|
768
770
|
</slot>
|
|
769
771
|
</div>
|
|
770
772
|
</template>
|
|
@@ -975,7 +977,8 @@
|
|
|
975
977
|
:cnRegistry="cnRegistry"
|
|
976
978
|
:availableWidgets="widgets"
|
|
977
979
|
@geoSaved="onGeoSaved"
|
|
978
|
-
@openIntegration="onAutoBodyOpenIntegration"
|
|
980
|
+
@openIntegration="onAutoBodyOpenIntegration"
|
|
981
|
+
@selectObject="onRelatedObjectSelect" />
|
|
979
982
|
</slot>
|
|
980
983
|
</div>
|
|
981
984
|
</aside>
|
|
@@ -2334,6 +2337,7 @@ export default {
|
|
|
2334
2337
|
'layout-change',
|
|
2335
2338
|
'open-integration',
|
|
2336
2339
|
'refresh',
|
|
2340
|
+
'related-object-click',
|
|
2337
2341
|
'related-row-click',
|
|
2338
2342
|
'relation-linked',
|
|
2339
2343
|
'request-feature',
|
|
@@ -4315,6 +4319,23 @@ export default {
|
|
|
4315
4319
|
this.$emit('open-integration', integrationId)
|
|
4316
4320
|
},
|
|
4317
4321
|
|
|
4322
|
+
/**
|
|
4323
|
+
* Re-emit a click on an object in the Related widget. The page does not
|
|
4324
|
+
* know the routes; the host (CnPageRenderer on a manifest page) opens
|
|
4325
|
+
* the object's own detail page.
|
|
4326
|
+
*
|
|
4327
|
+
* @param {object} raw The clicked object; `@self` names its register and schema.
|
|
4328
|
+
*/
|
|
4329
|
+
onRelatedObjectSelect(raw) {
|
|
4330
|
+
/**
|
|
4331
|
+
* @event related-object-click An object in the Related widget was
|
|
4332
|
+
* clicked. Payload is the raw object; `@self` carries its register,
|
|
4333
|
+
* schema and id.
|
|
4334
|
+
* @type {object}
|
|
4335
|
+
*/
|
|
4336
|
+
this.$emit('related-object-click', raw)
|
|
4337
|
+
},
|
|
4338
|
+
|
|
4318
4339
|
/**
|
|
4319
4340
|
* Re-fetch the object after the geo widget persists `@self.geo` (so the
|
|
4320
4341
|
* rest of the page — metadata, other widgets — reflects the new value),
|
|
@@ -114,7 +114,8 @@
|
|
|
114
114
|
:includeGroups="content.groups || []"
|
|
115
115
|
:hideSingleTabTitle="content.hideSingleTabTitle !== false"
|
|
116
116
|
:showTotalCount="content.showTotalCount !== false"
|
|
117
|
-
@openIntegration="onOpenIntegration"
|
|
117
|
+
@openIntegration="onOpenIntegration"
|
|
118
|
+
@selectObject="onSelectObject" />
|
|
118
119
|
|
|
119
120
|
<!-- `type: 'object-geo'` — view/edit the object's `@self.geo` on a map. -->
|
|
120
121
|
<CnObjectGeoWidget
|
|
@@ -183,7 +184,8 @@
|
|
|
183
184
|
<component
|
|
184
185
|
:is="renderer"
|
|
185
186
|
ref="renderer"
|
|
186
|
-
v-bind="rendererProps"
|
|
187
|
+
v-bind="rendererProps"
|
|
188
|
+
@selectObject="onSelectObject" />
|
|
187
189
|
</CnWidgetWrapper>
|
|
188
190
|
|
|
189
191
|
<!-- Registry "card" widgets (stat / gauge / delta) render bare tile
|
|
@@ -218,7 +220,8 @@
|
|
|
218
220
|
:is="renderer"
|
|
219
221
|
v-else-if="renderer"
|
|
220
222
|
ref="renderer"
|
|
221
|
-
v-bind="rendererProps"
|
|
223
|
+
v-bind="rendererProps"
|
|
224
|
+
@selectObject="onSelectObject" />
|
|
222
225
|
|
|
223
226
|
<!-- No renderer resolves: render nothing, which is what CnDetailPage has
|
|
224
227
|
always done here. Surfacing an "unknown widget" box instead would be
|
|
@@ -501,7 +504,7 @@ export default {
|
|
|
501
504
|
},
|
|
502
505
|
},
|
|
503
506
|
|
|
504
|
-
emits: ['geo-saved', 'open-integration'],
|
|
507
|
+
emits: ['geo-saved', 'open-integration', 'select-object'],
|
|
505
508
|
|
|
506
509
|
setup() {
|
|
507
510
|
// Every integration component this host renders goes through the
|
|
@@ -1006,6 +1009,21 @@ export default {
|
|
|
1006
1009
|
this.$emit('open-integration', integrationId)
|
|
1007
1010
|
},
|
|
1008
1011
|
|
|
1012
|
+
/**
|
|
1013
|
+
* Pass a related-object click up; the page decides where it opens. Heard
|
|
1014
|
+
* from the Related widget itself and from a registry widget that nests a
|
|
1015
|
+
* host of its own (a Tabs widget with a Related tab).
|
|
1016
|
+
*
|
|
1017
|
+
* @param {object} raw The clicked object as the related widget holds it.
|
|
1018
|
+
*/
|
|
1019
|
+
onSelectObject(raw) {
|
|
1020
|
+
/**
|
|
1021
|
+
* @event select-object A related object was clicked in the related widget. Payload is the raw object; its `@self` names the register and schema.
|
|
1022
|
+
* @type {object}
|
|
1023
|
+
*/
|
|
1024
|
+
this.$emit('select-object', raw)
|
|
1025
|
+
},
|
|
1026
|
+
|
|
1009
1027
|
/**
|
|
1010
1028
|
* Delegate to the rendered list widget's own create affordance, so the
|
|
1011
1029
|
* card's Add action and the widget's footer button open the same form.
|
|
@@ -731,7 +731,7 @@
|
|
|
731
731
|
:currentPageSize="effectivePagination.limit || 20"
|
|
732
732
|
class="cn-index-page__pagination"
|
|
733
733
|
@pageChanged="onPageEvent"
|
|
734
|
-
@pageSizeChanged="
|
|
734
|
+
@pageSizeChanged="onPageSizeEvent" />
|
|
735
735
|
</div>
|
|
736
736
|
|
|
737
737
|
<!-- The open record, beside the list on a wide screen and instead
|
|
@@ -1057,7 +1057,7 @@ function namedFilterToQuery(values) {
|
|
|
1057
1057
|
* @event {object} row-aux-click — Table row or card middle-clicked. Payload: (row, event) — the row object and the native auxclick event
|
|
1058
1058
|
* @event {{ key: string, order: string }} sort — Column sort changed
|
|
1059
1059
|
* @event {number} page-changed — Pagination page changed
|
|
1060
|
-
* @event {number} page-size-changed — Pagination page size changed
|
|
1060
|
+
* @event {number} page-size-changed — Pagination page size changed. In self-fetch mode the list refetches page 1 at the new size first.
|
|
1061
1061
|
* @event {string[]} select — Selection changed. Payload: array of selected IDs
|
|
1062
1062
|
* @event {object} action — Row action triggered. Payload: { action, row }
|
|
1063
1063
|
* @event {{ action: string, id: string, selectedIds: Array, count: number }} bulk-action — A declarative bulk action was triggered from the selection strip. The SELECTION travels with it: an action that has to go and find out what was selected is one re-render away from acting on a different set than the user saw highlighted.
|
|
@@ -5482,6 +5482,21 @@ export default {
|
|
|
5482
5482
|
this.$emit('page-changed', page)
|
|
5483
5483
|
},
|
|
5484
5484
|
|
|
5485
|
+
/**
|
|
5486
|
+
* Page-size pick from CnPagination. In self-fetch mode the list refetches
|
|
5487
|
+
* page 1 at the new size — the event alone left the select changing and
|
|
5488
|
+
* the table not, since a manifest page has no host listening.
|
|
5489
|
+
*
|
|
5490
|
+
* @param {number} size Requested page size.
|
|
5491
|
+
* @return {void}
|
|
5492
|
+
*/
|
|
5493
|
+
onPageSizeEvent(size) {
|
|
5494
|
+
if (this.isSelfFetchMode && typeof this.list.onPageSizeChange === 'function') {
|
|
5495
|
+
this.list.onPageSizeChange(size)
|
|
5496
|
+
}
|
|
5497
|
+
this.$emit('page-size-changed', size)
|
|
5498
|
+
},
|
|
5499
|
+
|
|
5485
5500
|
/**
|
|
5486
5501
|
* @param {{key: string, values: Array}} payload Facet-filter change from the sidebar.
|
|
5487
5502
|
* @return {void}
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
:modelValue="entry.enabled"
|
|
33
33
|
:disabled="entry.saving"
|
|
34
34
|
@update:modelValue="onToggle(entry, $event)">
|
|
35
|
-
{{ notificationLabel(entry
|
|
35
|
+
{{ notificationLabel(entry) }}
|
|
36
36
|
</NcCheckboxRadioSwitch>
|
|
37
37
|
<NcButton v-if="entry.source === 'user-override'"
|
|
38
38
|
variant="tertiary"
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
<script>
|
|
51
51
|
import axios from '@nextcloud/axios'
|
|
52
|
-
import { translate as t } from '@nextcloud/l10n'
|
|
52
|
+
import { getLanguage, translate as t } from '@nextcloud/l10n'
|
|
53
53
|
import { generateUrl } from '@nextcloud/router'
|
|
54
54
|
/**
|
|
55
55
|
* CnNotificationPreferences
|
|
@@ -82,6 +82,7 @@ import { generateUrl } from '@nextcloud/router'
|
|
|
82
82
|
import { NcAppSettingsSection, NcButton, NcCheckboxRadioSwitch, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
|
|
83
83
|
import BellOffOutline from 'vue-material-design-icons/BellOffOutline.vue'
|
|
84
84
|
import BellOutline from 'vue-material-design-icons/BellOutline.vue'
|
|
85
|
+
import { notificationRuleLabel } from '../../utils/notificationRuleLabel.js'
|
|
85
86
|
|
|
86
87
|
const PREFS_PATH = '/apps/openregister/api/notification-preferences'
|
|
87
88
|
|
|
@@ -106,6 +107,28 @@ export default {
|
|
|
106
107
|
* than an always-empty pane.
|
|
107
108
|
*/
|
|
108
109
|
cnAppId: { default: () => '' },
|
|
110
|
+
/**
|
|
111
|
+
* App-supplied labels for its notification rules, provided by the
|
|
112
|
+
* CnAppRoot ancestor from its `notificationLabels` prop. Keyed
|
|
113
|
+
* `<schema>.<key>` or `<key>`.
|
|
114
|
+
*/
|
|
115
|
+
cnNotificationLabels: { default: () => ({}) },
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
props: {
|
|
119
|
+
/**
|
|
120
|
+
* Labels for the notification rules, keyed `<schema>.<key>` or
|
|
121
|
+
* `<key>`; values are strings or per-locale maps (`{ nl, en }`).
|
|
122
|
+
* Optional: inside CnAppRoot the app's `notificationLabels` arrive by
|
|
123
|
+
* inject, and a rule without a label still reads as words, never as
|
|
124
|
+
* its raw key. A prop given here wins over the injected map.
|
|
125
|
+
*
|
|
126
|
+
* @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md
|
|
127
|
+
*/
|
|
128
|
+
labels: {
|
|
129
|
+
type: Object,
|
|
130
|
+
default: null,
|
|
131
|
+
},
|
|
109
132
|
},
|
|
110
133
|
|
|
111
134
|
data() {
|
|
@@ -231,18 +254,43 @@ export default {
|
|
|
231
254
|
},
|
|
232
255
|
|
|
233
256
|
/**
|
|
234
|
-
* Human-readable label for a notification
|
|
257
|
+
* Human-readable label for a notification rule. Never the raw key: the
|
|
258
|
+
* rule's own label, then the app's, then the library's wording for the
|
|
259
|
+
* generic keys, then the key split into words.
|
|
235
260
|
*
|
|
236
|
-
* @param {
|
|
261
|
+
* @param {object} entry The preference row.
|
|
237
262
|
* @return {string} A display label.
|
|
263
|
+
*
|
|
264
|
+
* @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md
|
|
238
265
|
*/
|
|
239
|
-
notificationLabel(
|
|
266
|
+
notificationLabel(entry) {
|
|
240
267
|
const known = {
|
|
241
268
|
object_created: t('nextcloud-vue', 'When an item is created'),
|
|
242
269
|
object_updated: t('nextcloud-vue', 'When an item is updated'),
|
|
243
270
|
object_transitioned: t('nextcloud-vue', 'When an item is assigned or changes status'),
|
|
244
271
|
}
|
|
245
|
-
|
|
272
|
+
const language = this.currentLanguage()
|
|
273
|
+
const injected = typeof this.cnNotificationLabels === 'function'
|
|
274
|
+
? this.cnNotificationLabels()
|
|
275
|
+
: this.cnNotificationLabels
|
|
276
|
+
return notificationRuleLabel(entry, {
|
|
277
|
+
labels: { ...(injected || {}), ...(this.labels || {}) },
|
|
278
|
+
known,
|
|
279
|
+
language,
|
|
280
|
+
})
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The person's language, `en` when it cannot be read.
|
|
285
|
+
*
|
|
286
|
+
* @return {string} The language code.
|
|
287
|
+
*/
|
|
288
|
+
currentLanguage() {
|
|
289
|
+
try {
|
|
290
|
+
return (typeof getLanguage === 'function' && getLanguage()) || 'en'
|
|
291
|
+
} catch {
|
|
292
|
+
return 'en'
|
|
293
|
+
}
|
|
246
294
|
},
|
|
247
295
|
|
|
248
296
|
/**
|
|
@@ -120,7 +120,8 @@
|
|
|
120
120
|
v-if="widgetsBySlot.has('body')"
|
|
121
121
|
:widgets="widgetsBySlot.get('body')"
|
|
122
122
|
:editable="bodyEditable"
|
|
123
|
-
slotName="body"
|
|
123
|
+
slotName="body"
|
|
124
|
+
@selectObject="onRelatedObjectOpen" />
|
|
124
125
|
<component
|
|
125
126
|
:is="resolvedComponent"
|
|
126
127
|
v-else-if="resolvedComponent"
|
|
@@ -130,6 +131,7 @@
|
|
|
130
131
|
@rowClick="onRowOpen"
|
|
131
132
|
@rowAuxClick="onRowOpen"
|
|
132
133
|
@editOpen="onRowOpen"
|
|
134
|
+
@relatedObjectClick="onRelatedObjectOpen"
|
|
133
135
|
@configure="showConfigModal = true">
|
|
134
136
|
<!-- This `<template v-for>` defines dynamic SLOTS, not a
|
|
135
137
|
rendered list, so the rule's advice is inverted here:
|
|
@@ -173,27 +175,32 @@
|
|
|
173
175
|
<CnWidgetGrid
|
|
174
176
|
v-if="widgetsBySlot.has('header-actions')"
|
|
175
177
|
:widgets="widgetsBySlot.get('header-actions')"
|
|
176
|
-
slotName="header-actions"
|
|
178
|
+
slotName="header-actions"
|
|
179
|
+
@selectObject="onRelatedObjectOpen" />
|
|
177
180
|
<!-- footer slot -->
|
|
178
181
|
<CnWidgetGrid
|
|
179
182
|
v-if="widgetsBySlot.has('footer')"
|
|
180
183
|
:widgets="widgetsBySlot.get('footer')"
|
|
181
|
-
slotName="footer"
|
|
184
|
+
slotName="footer"
|
|
185
|
+
@selectObject="onRelatedObjectOpen" />
|
|
182
186
|
<!-- modal slot -->
|
|
183
187
|
<CnWidgetGrid
|
|
184
188
|
v-if="widgetsBySlot.has('modal')"
|
|
185
189
|
:widgets="widgetsBySlot.get('modal')"
|
|
186
|
-
slotName="modal"
|
|
190
|
+
slotName="modal"
|
|
191
|
+
@selectObject="onRelatedObjectOpen" />
|
|
187
192
|
<!-- sidebar slot (gated by cnPageSidebarVisible) -->
|
|
188
193
|
<CnWidgetGrid
|
|
189
194
|
v-if="widgetsBySlot.has('sidebar') && pageSidebarVisibleValue"
|
|
190
195
|
:widgets="widgetsBySlot.get('sidebar')"
|
|
191
|
-
slotName="sidebar"
|
|
196
|
+
slotName="sidebar"
|
|
197
|
+
@selectObject="onRelatedObjectOpen" />
|
|
192
198
|
<!-- dynamic tab:* and section:* slots -->
|
|
193
199
|
<template v-for="dynamicSlot in dynamicSlotKeys" :key="dynamicSlot">
|
|
194
200
|
<CnWidgetGrid
|
|
195
201
|
:widgets="widgetsBySlot.get(dynamicSlot)"
|
|
196
|
-
:slotName="dynamicSlot"
|
|
202
|
+
:slotName="dynamicSlot"
|
|
203
|
+
@selectObject="onRelatedObjectOpen" />
|
|
197
204
|
</template>
|
|
198
205
|
</template>
|
|
199
206
|
|
|
@@ -206,6 +213,7 @@
|
|
|
206
213
|
@rowClick="onRowOpen"
|
|
207
214
|
@rowAuxClick="onRowOpen"
|
|
208
215
|
@editOpen="onRowOpen"
|
|
216
|
+
@relatedObjectClick="onRelatedObjectOpen"
|
|
209
217
|
@configure="showConfigModal = true">
|
|
210
218
|
<!-- Dynamic slot definition, not a rendered list — see the note on
|
|
211
219
|
the identical block above. -->
|
|
@@ -300,6 +308,53 @@ import { resolveRouteSentinels } from '../../utils/resolveRouteSentinels.js'
|
|
|
300
308
|
import { parseSortKeys } from '../../utils/routeFilters.js'
|
|
301
309
|
import { buildRouteParams, routePathFor } from '../../utils/routeParams.js'
|
|
302
310
|
import { isNewTabHandled } from '../../utils/rowAuxClick.js'
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* OpenRegister registers and schemas already described by id, so a click on
|
|
314
|
+
* a related object costs at most one request per register or schema for as
|
|
315
|
+
* long as this module stays loaded (the SPA's lifetime, across page
|
|
316
|
+
* changes). Keyed by `${kind}:${id}`; a failed lookup is dropped again so the
|
|
317
|
+
* next click retries it.
|
|
318
|
+
*
|
|
319
|
+
* @type {Map<string, Promise<{ id: string, slug: string }>>}
|
|
320
|
+
*/
|
|
321
|
+
const entityDescriptorCache = new Map()
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Describe an OpenRegister register or schema (`{ id, slug }`) by its id or
|
|
325
|
+
* slug, or null when it cannot be fetched. An object's `@self` names its
|
|
326
|
+
* register and schema by id while a manifest may name them by slug; this is
|
|
327
|
+
* the bridge. Lazy-imports axios and the router helper like the widgets do.
|
|
328
|
+
*
|
|
329
|
+
* @param {'registers'|'schemas'} kind The collection to ask.
|
|
330
|
+
* @param {string|number} idOrSlug The id (or slug) to describe.
|
|
331
|
+
* @return {Promise<{ id: string, slug: string }|null>} The descriptor.
|
|
332
|
+
*/
|
|
333
|
+
function describeOpenRegisterEntity(kind, idOrSlug) {
|
|
334
|
+
const key = `${kind}:${idOrSlug}`
|
|
335
|
+
if (!entityDescriptorCache.has(key)) {
|
|
336
|
+
entityDescriptorCache.set(key, (async () => {
|
|
337
|
+
try {
|
|
338
|
+
const [{ default: axios }, { generateUrl }] = await Promise.all([
|
|
339
|
+
import('@nextcloud/axios'),
|
|
340
|
+
import('@nextcloud/router'),
|
|
341
|
+
])
|
|
342
|
+
const url = generateUrl('/apps/openregister/api/{kind}/{id}', { kind, id: String(idOrSlug) })
|
|
343
|
+
const res = await axios.get(url)
|
|
344
|
+
const data = res && res.data
|
|
345
|
+
if (!data || typeof data !== 'object') {
|
|
346
|
+
throw new Error(`no descriptor for ${key}`)
|
|
347
|
+
}
|
|
348
|
+
return { id: String(data.id ?? idOrSlug), slug: String(data.slug ?? '') }
|
|
349
|
+
} catch (error) {
|
|
350
|
+
// One failed request must not pin every later click to nowhere.
|
|
351
|
+
entityDescriptorCache.delete(key)
|
|
352
|
+
throw error
|
|
353
|
+
}
|
|
354
|
+
})())
|
|
355
|
+
}
|
|
356
|
+
return entityDescriptorCache.get(key).catch(() => null)
|
|
357
|
+
}
|
|
303
358
|
import { CnMassExportDialog } from '../CnMassExportDialog/index.js'
|
|
304
359
|
import { defaultPageTypes } from './pageTypes.js'
|
|
305
360
|
|
|
@@ -1830,6 +1885,92 @@ export default {
|
|
|
1830
1885
|
})
|
|
1831
1886
|
},
|
|
1832
1887
|
|
|
1888
|
+
/**
|
|
1889
|
+
* Open the detail page of an object clicked in a Related widget —
|
|
1890
|
+
* reaching here as `CnDetailPage`'s `related-object-click`, or as
|
|
1891
|
+
* `CnWidgetGrid`'s `select-object` on a v2 widget-grid page. Nothing
|
|
1892
|
+
* listened to either, so the rows looked like links and went nowhere.
|
|
1893
|
+
*
|
|
1894
|
+
* @param {object} raw The clicked object as the Related widget holds it.
|
|
1895
|
+
* @return {Promise<void>}
|
|
1896
|
+
*/
|
|
1897
|
+
async onRelatedObjectOpen(raw) {
|
|
1898
|
+
const target = await this.relatedObjectTarget(raw)
|
|
1899
|
+
if (target) {
|
|
1900
|
+
this.$router.push(target).catch(() => {})
|
|
1901
|
+
}
|
|
1902
|
+
},
|
|
1903
|
+
|
|
1904
|
+
/**
|
|
1905
|
+
* Where a related object opens: the first `type: 'detail'` page bound
|
|
1906
|
+
* to the object's register and schema, with the id in whichever param
|
|
1907
|
+
* that page's route declares. The object's `@self` names register and
|
|
1908
|
+
* schema by id and a manifest may name them by slug, so a miss on the
|
|
1909
|
+
* literal pair describes both (one request each, cached) and matches
|
|
1910
|
+
* again. Null, silently, when there is no router or the object carries
|
|
1911
|
+
* no register, schema or id (nothing to open); null with a warning,
|
|
1912
|
+
* like a dead row click, when no detail page matches or the router
|
|
1913
|
+
* lacks the route.
|
|
1914
|
+
*
|
|
1915
|
+
* @param {object} raw The clicked object.
|
|
1916
|
+
* @return {Promise<object|null>} The router location, or null.
|
|
1917
|
+
*/
|
|
1918
|
+
async relatedObjectTarget(raw) {
|
|
1919
|
+
const router = this.$router
|
|
1920
|
+
if (!router || !raw || typeof raw !== 'object') {
|
|
1921
|
+
return null
|
|
1922
|
+
}
|
|
1923
|
+
const self = raw['@self'] || {}
|
|
1924
|
+
const id = raw.id ?? self.id ?? self.uuid ?? raw.uuid
|
|
1925
|
+
const register = self.register ?? raw.register
|
|
1926
|
+
const schema = self.schema ?? raw.schema
|
|
1927
|
+
const missing = (v) => v === undefined || v === null || v === ''
|
|
1928
|
+
if (missing(id) || missing(register) || missing(schema)) {
|
|
1929
|
+
return null
|
|
1930
|
+
}
|
|
1931
|
+
let page = this.detailPageFor([String(register)], [String(schema)])
|
|
1932
|
+
if (!page) {
|
|
1933
|
+
const [reg, sch] = await Promise.all([
|
|
1934
|
+
describeOpenRegisterEntity('registers', register),
|
|
1935
|
+
describeOpenRegisterEntity('schemas', schema),
|
|
1936
|
+
])
|
|
1937
|
+
page = this.detailPageFor(
|
|
1938
|
+
[String(register), reg?.id, reg?.slug].filter(Boolean),
|
|
1939
|
+
[String(schema), sch?.id, sch?.slug].filter(Boolean),
|
|
1940
|
+
)
|
|
1941
|
+
}
|
|
1942
|
+
if (!page) {
|
|
1943
|
+
// eslint-disable-next-line no-console
|
|
1944
|
+
console.warn(`[CnPageRenderer] No detail page is bound to register "${register}" and schema "${schema}", so related object "${id}" opens nowhere.`)
|
|
1945
|
+
return null
|
|
1946
|
+
}
|
|
1947
|
+
if (this.routeNameIsKnown(page.id) === false) {
|
|
1948
|
+
// eslint-disable-next-line no-console
|
|
1949
|
+
console.warn(`[CnPageRenderer] Detail page "${page.id}" is not a route the router has, so related object "${id}" opens nowhere.`)
|
|
1950
|
+
return null
|
|
1951
|
+
}
|
|
1952
|
+
const path = page.route ?? routePathFor(router, page.id)
|
|
1953
|
+
return { name: page.id, params: buildRouteParams(path, id, this.$route?.params) }
|
|
1954
|
+
},
|
|
1955
|
+
|
|
1956
|
+
/**
|
|
1957
|
+
* The first `type: 'detail'` page whose register and schema are both
|
|
1958
|
+
* among the given names (ids or slugs, compared as strings).
|
|
1959
|
+
*
|
|
1960
|
+
* @param {Array<string>} registerNames Names the register goes by.
|
|
1961
|
+
* @param {Array<string>} schemaNames Names the schema goes by.
|
|
1962
|
+
* @return {object|null} The page, or null.
|
|
1963
|
+
*/
|
|
1964
|
+
detailPageFor(registerNames, schemaNames) {
|
|
1965
|
+
for (const page of this.detailPageByRegisterSchema.values()) {
|
|
1966
|
+
const cfg = page.config || {}
|
|
1967
|
+
if (registerNames.includes(String(cfg.register)) && schemaNames.includes(String(cfg.schema))) {
|
|
1968
|
+
return page
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
return null
|
|
1972
|
+
},
|
|
1973
|
+
|
|
1833
1974
|
/**
|
|
1834
1975
|
* Whether the router has a route by this name.
|
|
1835
1976
|
*
|
|
@@ -88,7 +88,8 @@
|
|
|
88
88
|
:integrationContext="integrationContext"
|
|
89
89
|
:cnRegistry="cnRegistry"
|
|
90
90
|
@geoSaved="onGeoSaved"
|
|
91
|
-
@openIntegration="onOpenIntegration"
|
|
91
|
+
@openIntegration="onOpenIntegration"
|
|
92
|
+
@selectObject="onSelectObject" />
|
|
92
93
|
<NcEmptyContent v-else :name="missingLabel(entry)" />
|
|
93
94
|
</CnTab>
|
|
94
95
|
</CnTabs>
|
|
@@ -357,7 +358,7 @@ export default {
|
|
|
357
358
|
},
|
|
358
359
|
},
|
|
359
360
|
|
|
360
|
-
emits: ['geo-saved', 'open-integration'],
|
|
361
|
+
emits: ['geo-saved', 'open-integration', 'select-object'],
|
|
361
362
|
|
|
362
363
|
data() {
|
|
363
364
|
return {
|
|
@@ -530,6 +531,20 @@ export default {
|
|
|
530
531
|
this.$emit('open-integration', integrationId)
|
|
531
532
|
},
|
|
532
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Re-emit a related child's object click; the page opens the object.
|
|
536
|
+
*
|
|
537
|
+
* @param {object} raw The clicked object.
|
|
538
|
+
* @return {void}
|
|
539
|
+
*/
|
|
540
|
+
onSelectObject(raw) {
|
|
541
|
+
/**
|
|
542
|
+
* @event select-object Re-emitted from a related child whose object row was clicked. Payload is the raw object.
|
|
543
|
+
* @type {object}
|
|
544
|
+
*/
|
|
545
|
+
this.$emit('select-object', raw)
|
|
546
|
+
},
|
|
547
|
+
|
|
533
548
|
/**
|
|
534
549
|
* Follow the strip's own selection, so keyboard navigation moves the
|
|
535
550
|
* hoisted Actions menu too.
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
<div class="grid-stack-item-content">
|
|
40
40
|
<component
|
|
41
41
|
:is="widget.component"
|
|
42
|
-
v-bind="widget.props"
|
|
42
|
+
v-bind="widget.props"
|
|
43
|
+
@selectObject="onSelectObject" />
|
|
43
44
|
</div>
|
|
44
45
|
</div>
|
|
45
46
|
</div>
|
|
@@ -58,7 +59,8 @@
|
|
|
58
59
|
:style="cnGridCellStyle(widget, gridColumns)">
|
|
59
60
|
<component
|
|
60
61
|
:is="widget.component"
|
|
61
|
-
v-bind="widget.props"
|
|
62
|
+
v-bind="widget.props"
|
|
63
|
+
@selectObject="onSelectObject" />
|
|
62
64
|
</div>
|
|
63
65
|
</template>
|
|
64
66
|
</div>
|
|
@@ -161,7 +163,7 @@ export default {
|
|
|
161
163
|
},
|
|
162
164
|
},
|
|
163
165
|
|
|
164
|
-
emits: ['layout-change'],
|
|
166
|
+
emits: ['layout-change', 'select-object'],
|
|
165
167
|
|
|
166
168
|
data() {
|
|
167
169
|
return {
|
|
@@ -371,6 +373,20 @@ export default {
|
|
|
371
373
|
},
|
|
372
374
|
|
|
373
375
|
methods: {
|
|
376
|
+
/**
|
|
377
|
+
* Pass a related widget's object click up; the page opens the object.
|
|
378
|
+
*
|
|
379
|
+
* @param {object} raw The clicked object.
|
|
380
|
+
* @return {void}
|
|
381
|
+
*/
|
|
382
|
+
onSelectObject(raw) {
|
|
383
|
+
/**
|
|
384
|
+
* @event select-object An object row in a related widget of this grid was clicked. Payload is the raw object.
|
|
385
|
+
* @type {object}
|
|
386
|
+
*/
|
|
387
|
+
this.$emit('select-object', raw)
|
|
388
|
+
},
|
|
389
|
+
|
|
374
390
|
// Expose the shared grid helpers to the template.
|
|
375
391
|
cnGridCellStyle,
|
|
376
392
|
hasGridRow,
|
package/src/index.js
CHANGED
|
@@ -476,6 +476,8 @@ export {
|
|
|
476
476
|
} from './utils/folderCustomization.js'
|
|
477
477
|
export { currentTheme, useCurrentTheme } from './composables/useCurrentTheme.js'
|
|
478
478
|
export { ROADMAP_LABEL_BLOCKLIST } from './utils/roadmapLabelBlocklist.js'
|
|
479
|
+
// The words a person reads for a notification rule key (never the raw key).
|
|
480
|
+
export { humaniseRuleKey, notificationRuleLabel } from './utils/notificationRuleLabel.js'
|
|
479
481
|
|
|
480
482
|
// Shared field validators. These are UX affordances that run in the browser —
|
|
481
483
|
// they do NOT replace server-side validation, which stays authoritative at the
|