@conduction/nextcloud-vue 2.65.0 → 2.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +99 -2
  2. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  3. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +438 -383
  4. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  5. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +9 -3
  6. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -1
  7. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +1 -1
  8. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -1
  9. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +78 -21
  10. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  11. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +129 -96
  12. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  13. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js +3 -1
  14. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js.map +1 -1
  15. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +144 -4
  16. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  17. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  18. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +63 -7
  19. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  20. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +2 -1
  21. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  22. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +11 -0
  23. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  24. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +2 -1
  25. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  26. package/dist/esm/l10n/en.json.js +3 -0
  27. package/dist/esm/l10n/en.json.js.map +1 -1
  28. package/dist/esm/l10n/nl.json.js +3 -0
  29. package/dist/esm/l10n/nl.json.js.map +1 -1
  30. package/dist/esm/mixins/pageViews.js +400 -0
  31. package/dist/esm/mixins/pageViews.js.map +1 -0
  32. package/dist/esm/nextcloud-vue.css +18 -0
  33. package/dist/esm/utils/actionsDispatcher.js +11 -2
  34. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  35. package/dist/esm/utils/columnFilters.js +27 -3
  36. package/dist/esm/utils/columnFilters.js.map +1 -1
  37. package/dist/esm/utils/schema.js +40 -2
  38. package/dist/esm/utils/schema.js.map +1 -1
  39. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  40. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  41. package/dist/nextcloud-vue.cjs.js +1521 -596
  42. package/dist/nextcloud-vue.cjs.js.map +2 -2
  43. package/dist/nextcloud-vue.css +18 -0
  44. package/l10n/en.json +3 -0
  45. package/l10n/nl.json +3 -0
  46. package/package.json +1 -1
  47. package/src/components/CnDashboardPage/CnDashboardPage.vue +165 -8
  48. package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
  49. package/src/components/CnDetailPage/CnDetailPage.vue +94 -5
  50. package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
  51. package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
  52. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +67 -8
  53. package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
  54. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
  55. package/src/css/detail-page.css +5 -0
  56. package/src/mixins/pageViews.js +396 -0
  57. package/src/schemas/app-manifest-v2.schema.json +29 -1
  58. package/src/utils/actionsDispatcher.js +11 -2
  59. package/src/utils/columnFilters.js +29 -2
  60. package/src/utils/schema.js +40 -2
  61. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -513,6 +513,7 @@ import { draftKey, formDraftMixin, readDraft } from '../../composables/useFormDr
513
513
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
514
514
  import { TENANT_CONTEXT_KEY } from '../../composables/useTenantContext.js'
515
515
  import { useObjectStore } from '../../store/useObjectStore.js'
516
+ import { resolveCreateOverrideHandler } from '../../utils/actionsDispatcher.js'
516
517
  import {
517
518
  definitionQueryParams,
518
519
  DYNAMIC_KEY_PREFIX,
@@ -757,6 +758,18 @@ export default {
757
758
  * identity when used standalone (no CnAppRoot ancestor).
758
759
  */
759
760
  cnTranslate: { default: () => (key) => key },
761
+
762
+ /**
763
+ * The app manifest, the v2 registry, the legacy customComponents map and
764
+ * the registry-modal opener, all provided by CnAppRoot. A select-or-create
765
+ * picker reads them to create an object the way the app's own index page
766
+ * does (`config.createOverride` / `config.createModal`). Standalone (no
767
+ * CnAppRoot) they are absent and the generic nested form is used.
768
+ */
769
+ cnManifest: { default: null },
770
+ cnRegistry: { default: null },
771
+ cnCustomComponents: { default: null },
772
+ cnOpenModal: { default: null },
760
773
  },
761
774
 
762
775
  props: {
@@ -2649,6 +2662,16 @@ export default {
2649
2662
  store.registerObjectType(slug, schemaRef, register)
2650
2663
  }
2651
2664
  const labelField = this.referenceLabelField(field)
2665
+ // The app may own how this schema is created (pipelinq's client and
2666
+ // contact need a contact-first endpoint; a plain save 400s). A
2667
+ // create override keeps this form and saves through the app's
2668
+ // handler; a create modal (only when no override exists) opens the
2669
+ // app's own dialog instead of the generic form.
2670
+ const appCreate = this.appCreateFor(register, schemaRef)
2671
+ if (!appCreate.override && appCreate.modal) {
2672
+ return this.openAppCreateModal(appCreate.modal, store, slug, labelField, term)
2673
+ }
2674
+ const override = appCreate.override
2652
2675
  let schema = null
2653
2676
  try {
2654
2677
  schema = await store.fetchSchema(slug)
@@ -2656,14 +2679,113 @@ export default {
2656
2679
  schema = null
2657
2680
  }
2658
2681
  if (!schema || !schema.properties) {
2659
- return store.saveObject(slug, { [labelField]: term })
2682
+ const payload = { [labelField]: term }
2683
+ return override
2684
+ ? override(payload, { register, schema: schemaRef, objectType: slug, effectiveSchema: null })
2685
+ : store.saveObject(slug, payload)
2660
2686
  }
2661
2687
  const initialData = {}
2662
2688
  if (term && schema.properties[labelField]) {
2663
2689
  initialData[labelField] = term
2664
2690
  }
2665
2691
  return new Promise((resolve) => {
2666
- this.nestedCreate = { field, schema, register, slug, initialData, resolve }
2692
+ this.nestedCreate = { field, schema, register, slug, schemaRef, initialData, override, resolve }
2693
+ })
2694
+ },
2695
+
2696
+ /**
2697
+ * How the app creates objects of a schema, read off the manifest page
2698
+ * that lists them: the index page whose `config.schema` (and, when it
2699
+ * names one, `config.register`) matches. Its `config.createOverride` is
2700
+ * resolved to the registered handler; its `config.createModal` is the
2701
+ * registry key of a `kind: 'modal'` dialog.
2702
+ *
2703
+ * @spec openspec/changes/review-round-two/specs/schema-utilities/spec.md
2704
+ * @param {string} register The reference's register.
2705
+ * @param {string} schemaRef The reference's schema slug or id.
2706
+ * @return {{override: (((formData: object, ctx: object) => Promise<object>)|null), modal: (string|null)}} What the app declared.
2707
+ */
2708
+ appCreateFor(register, schemaRef) {
2709
+ const none = { override: null, modal: null }
2710
+ const pages = this.cnManifest && Array.isArray(this.cnManifest.pages) ? this.cnManifest.pages : []
2711
+ const wanted = String(schemaRef).toLowerCase()
2712
+ const page = pages.find((p) => {
2713
+ const config = p && p.config
2714
+ if (!config || config.schema === undefined || config.schema === null) {
2715
+ return false
2716
+ }
2717
+ if (String(config.schema).toLowerCase() !== wanted) {
2718
+ return false
2719
+ }
2720
+ return !config.register || String(config.register) === String(register)
2721
+ })
2722
+ if (!page) {
2723
+ return none
2724
+ }
2725
+ const config = page.config
2726
+ const override = typeof config.createOverride === 'function'
2727
+ ? config.createOverride
2728
+ : resolveCreateOverrideHandler(config.createOverride, this.cnRegistry, this.cnCustomComponents)
2729
+ const modalEntry = typeof config.createModal === 'string' && config.createModal !== ''
2730
+ ? (this.cnRegistry || {})[config.createModal]
2731
+ : null
2732
+ const modal = modalEntry && modalEntry.kind === 'modal' && typeof this.cnOpenModal === 'function'
2733
+ ? config.createModal
2734
+ : null
2735
+ return { override: override || null, modal }
2736
+ },
2737
+
2738
+ /**
2739
+ * Open the app's own create dialog (a registry modal) for a picker and
2740
+ * wait for it. The dialog reports the new object (or its id) with a
2741
+ * `created` event; `close` without one resolves null so the field stays
2742
+ * as it was. A reported id is fetched so the picker can label it.
2743
+ *
2744
+ * @spec openspec/changes/review-round-two/specs/schema-utilities/spec.md
2745
+ * @param {string} key The registry key of the modal.
2746
+ * @param {object} store The object store.
2747
+ * @param {string} slug The object type slug.
2748
+ * @param {string} labelField The property the typed term belongs in.
2749
+ * @param {string} term The typed search term.
2750
+ * @return {Promise<object|null>} The created object, or null.
2751
+ */
2752
+ openAppCreateModal(key, store, slug, labelField, term) {
2753
+ return new Promise((resolve) => {
2754
+ let settled = false
2755
+ let reported = false
2756
+ const settle = (value) => {
2757
+ if (!settled) {
2758
+ settled = true
2759
+ resolve(value)
2760
+ }
2761
+ }
2762
+ this.cnOpenModal(key, {
2763
+ initialData: term ? { [labelField]: term } : {},
2764
+ onCreated: async (created) => {
2765
+ if (created && typeof created === 'object') {
2766
+ settle(created)
2767
+ return
2768
+ }
2769
+ if (typeof created !== 'string' && typeof created !== 'number') {
2770
+ return
2771
+ }
2772
+ reported = true
2773
+ let obj
2774
+ try {
2775
+ obj = await store.fetchObject(slug, String(created))
2776
+ } catch {
2777
+ obj = null
2778
+ }
2779
+ settle(obj || { id: String(created), [labelField]: term })
2780
+ },
2781
+ onClose: () => {
2782
+ // A dialog often emits `close` right after `created`,
2783
+ // while the id is still being fetched: that fetch settles.
2784
+ if (!reported) {
2785
+ settle(null)
2786
+ }
2787
+ },
2788
+ })
2667
2789
  })
2668
2790
  },
2669
2791
 
@@ -2682,10 +2804,28 @@ export default {
2682
2804
  return
2683
2805
  }
2684
2806
  const store = this.getObjectStore()
2685
- const created = store ? await store.saveObject(pending.slug, formData) : null
2807
+ let created = null
2808
+ let thrown = null
2809
+ if (pending.override) {
2810
+ // The app's create override owns persistence (same contract as
2811
+ // CnIndexPage's `createOverride`).
2812
+ try {
2813
+ created = await pending.override(formData, {
2814
+ register: pending.register,
2815
+ schema: pending.schemaRef,
2816
+ objectType: pending.slug,
2817
+ effectiveSchema: pending.schema,
2818
+ })
2819
+ } catch (err) {
2820
+ thrown = (err && err.response && err.response.data && err.response.data.error) || (err && err.message) || null
2821
+ }
2822
+ } else {
2823
+ created = store ? await store.saveObject(pending.slug, formData) : null
2824
+ }
2686
2825
  if (!created) {
2687
2826
  const error = store && store.errors ? store.errors[pending.slug] : null
2688
- const message = (error && error.message)
2827
+ const message = thrown
2828
+ || (!pending.override && error && error.message)
2689
2829
  || t('nextcloud-vue', 'The item could not be created.')
2690
2830
  const dialog = this.$refs.nestedCreateDialog
2691
2831
  if (dialog && typeof dialog.setValidationErrors === 'function') {
@@ -35,12 +35,15 @@
35
35
  </a>
36
36
  </div>
37
37
  <!-- A view switch at the right of the heading (`content.views`): "My
38
- work / My team" as a segmented control whose options are routes. -->
38
+ work / My team" as a segmented control whose options are routes,
39
+ or views of the page the widget sits on (`option.view`); a view
40
+ option controls the page's view region. -->
39
41
  <div v-if="viewOptions.length > 0" class="cn-header-widget__views">
40
42
  <CnSegmentedControl
41
43
  :options="viewOptions"
42
44
  :modelValue="activeView"
43
45
  :ariaLabel="viewsLabel"
46
+ :controls="viewsRegionId"
44
47
  data-testid="cn-header-widget-views"
45
48
  @update:modelValue="onViewChange" />
46
49
  </div>
@@ -66,6 +69,9 @@ const ALLOWED_TEXT_ALIGN = ['left', 'center', 'right']
66
69
  const ALLOWED_VERTICAL_ALIGN = ['top', 'middle', 'bottom']
67
70
  const ALLOWED_CTA_STYLES = ['primary', 'secondary', 'ghost']
68
71
 
72
+ /** Marks a switch option that selects a page view rather than a route. */
73
+ const VIEW_OPTION_PREFIX = 'view:'
74
+
69
75
  const HEIGHT_PIXELS = Object.freeze({
70
76
  small: 120,
71
77
  medium: 200,
@@ -121,6 +127,12 @@ export default {
121
127
  * untranslated key renders as itself.
122
128
  */
123
129
  cnTranslate: { default: () => (key) => key },
130
+ /**
131
+ * The views of the page this widget sits on, provided by
132
+ * CnDashboardPage / CnDetailPage (`views`). An option with a `view`
133
+ * selects one of them. Null outside such a page.
134
+ */
135
+ cnPageViews: { default: null },
124
136
  },
125
137
 
126
138
  props: {
@@ -131,9 +143,12 @@ export default {
131
143
  * greeting, showDate, plain, ground, views}`. All fields are optional except
132
144
  * `title` (or `greeting`); unknown enum values collapse to documented
133
145
  * defaults and the renderer never throws. `views` is
134
- * `{ ariaLabel?, options: [{ label, route, params? }] }`: a segmented
135
- * control at the right of the heading whose checked option is the
136
- * current route; choosing another pushes its route. `ground: true`
146
+ * `{ ariaLabel?, options: [{ label, route, params? } | { label, view }] }`:
147
+ * a segmented control at the right of the heading whose checked option
148
+ * is the current route; choosing another pushes its route. An option
149
+ * with `view` selects that view of the page the widget sits on (the
150
+ * page's `views`) instead, and the page then draws no switch of its
151
+ * own. `ground: true`
137
152
  * draws the greeting on the page ground: the plain look with no
138
153
  * padding, a 32px heading and the date line 6px above it (a dashboard
139
154
  * also drops the widget's card for it).
@@ -229,12 +244,45 @@ export default {
229
244
  */
230
245
  viewOptions() {
231
246
  const raw = this.content && this.content.views && this.content.views.options
232
- if (!Array.isArray(raw) || !this.$router) {
247
+ if (!Array.isArray(raw)) {
233
248
  return []
234
249
  }
250
+ const pageViewIds = this.pageViewIds
235
251
  return raw
236
- .filter((o) => o && typeof o.label === 'string' && o.label !== '' && typeof o.route === 'string' && o.route !== '')
237
- .map((o) => ({ value: o.route, label: this.cnTranslate(o.label) }))
252
+ .filter((o) => o && typeof o.label === 'string' && o.label !== '')
253
+ .map((o) => {
254
+ if (typeof o.view === 'string' && o.view !== '') {
255
+ return pageViewIds.includes(o.view) ? { value: VIEW_OPTION_PREFIX + o.view, label: this.cnTranslate(o.label) } : null
256
+ }
257
+ if (typeof o.route === 'string' && o.route !== '' && this.$router) {
258
+ return { value: o.route, label: this.cnTranslate(o.label) }
259
+ }
260
+ return null
261
+ })
262
+ .filter(Boolean)
263
+ },
264
+
265
+ /**
266
+ * The ids of the page's views, empty outside a page with views.
267
+ *
268
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-greeting-header-can-switch-the-pages-views
269
+ * @return {string[]}
270
+ */
271
+ pageViewIds() {
272
+ const views = this.cnPageViews && Array.isArray(this.cnPageViews.views) ? this.cnPageViews.views : []
273
+ return views.map((view) => view.id)
274
+ },
275
+
276
+ /**
277
+ * The id of the page's view region when an option selects a view,
278
+ * for the options' `aria-controls`; '' otherwise.
279
+ *
280
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible
281
+ * @return {string}
282
+ */
283
+ viewsRegionId() {
284
+ const hasViewOption = this.viewOptions.some((o) => o.value.startsWith(VIEW_OPTION_PREFIX))
285
+ return hasViewOption && this.cnPageViews ? (this.cnPageViews.regionId || '') : ''
238
286
  },
239
287
 
240
288
  /**
@@ -249,6 +297,11 @@ export default {
249
297
  if (options.length === 0) {
250
298
  return null
251
299
  }
300
+ const activeView = this.cnPageViews ? this.cnPageViews.activeId : null
301
+ const viewMatch = activeView ? options.find((o) => o.value === VIEW_OPTION_PREFIX + activeView) : null
302
+ if (viewMatch) {
303
+ return viewMatch.value
304
+ }
252
305
  const current = this.$route && this.$route.name
253
306
  const match = options.find((o) => o.value === current)
254
307
  return match ? match.value : options[0].value
@@ -263,7 +316,7 @@ export default {
263
316
  */
264
317
  viewsLabel() {
265
318
  const label = this.content && this.content.views && this.content.views.ariaLabel
266
- return label ? this.cnTranslate(label) : t('nextcloud-vue', 'View')
319
+ return label ? this.cnTranslate(label) : t('nextcloud-vue', 'Views')
267
320
  },
268
321
 
269
322
  /**
@@ -697,6 +750,12 @@ export default {
697
750
  * @return {void}
698
751
  */
699
752
  onViewChange(route) {
753
+ if (typeof route === 'string' && route.startsWith(VIEW_OPTION_PREFIX)) {
754
+ if (this.cnPageViews && typeof this.cnPageViews.select === 'function') {
755
+ this.cnPageViews.select(route.slice(VIEW_OPTION_PREFIX.length))
756
+ }
757
+ return
758
+ }
700
759
  if (!this.$router || !route || route === (this.$route && this.$route.name)) {
701
760
  return
702
761
  }
@@ -43,4 +43,4 @@ export default {
43
43
  </script>
44
44
  ```
45
45
 
46
- Props: `options` lists the choices, `modelValue` is the chosen value (use `v-model`), `aria-label` names the group (or point `aria-labelledby` at a visible heading), and `stretch` fills the width.
46
+ Props: `options` lists the choices, `modelValue` is the chosen value (use `v-model`), `aria-label` names the group (or point `aria-labelledby` at a visible heading), `controls` names the id of the element the options switch (set as `aria-controls` on every option), and `stretch` fills the width.
@@ -20,6 +20,7 @@
20
20
  :aria-checked="isChecked(option) ? 'true' : 'false'"
21
21
  :tabindex="index === focusableIndex ? 0 : -1"
22
22
  :disabled="option.disabled || null"
23
+ :aria-controls="controls || null"
23
24
  :data-value="option.key"
24
25
  @click="select(option)">
25
26
  <!-- @slot option Replaces the content of one option. -->
@@ -95,6 +96,17 @@ export default {
95
96
  default: '',
96
97
  },
97
98
 
99
+ /**
100
+ * Id of the element the options switch, such as the region holding a
101
+ * page view's widgets. Set as `aria-controls` on every option.
102
+ *
103
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible
104
+ */
105
+ controls: {
106
+ type: String,
107
+ default: '',
108
+ },
109
+
98
110
  /** Stretch the control to the full width, with options of equal width. */
99
111
  stretch: {
100
112
  type: Boolean,
@@ -481,3 +481,8 @@
481
481
  font-size: 0.9em;
482
482
  white-space: nowrap;
483
483
  }
484
+
485
+ /* A page view the switch shows with nothing to draw (view-switch-containers). */
486
+ .cn-detail-page .cn-page-view-region--empty {
487
+ padding: 40px 20px;
488
+ }