@conduction/nextcloud-vue 2.127.0 → 2.128.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 (45) hide show
  1. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  2. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +3 -113
  3. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  4. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +12 -12
  5. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  6. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +34 -1
  7. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  8. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +16 -8
  9. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  10. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +153 -8
  11. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  12. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +169 -58
  13. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  14. package/dist/esm/components/CnTileWidget/CnTileWidget.vue.js +1 -1
  15. package/dist/esm/components/CnTileWidget/CnTileWidget.vue2.js +11 -0
  16. package/dist/esm/components/CnTileWidget/CnTileWidget.vue2.js.map +1 -1
  17. package/dist/esm/components/CnTileWidget/CnTileWidget.vue3.js +14 -4
  18. package/dist/esm/components/CnTileWidget/CnTileWidget.vue3.js.map +1 -1
  19. package/dist/esm/library-style.js +1 -1
  20. package/dist/esm/mixins/pageFilters.js +142 -0
  21. package/dist/esm/mixins/pageFilters.js.map +1 -0
  22. package/dist/esm/nextcloud-vue.css +269 -74
  23. package/dist/esm/utils/actionLink.js +9 -3
  24. package/dist/esm/utils/actionLink.js.map +1 -1
  25. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  26. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  27. package/dist/nextcloud-vue.cjs.js +1176 -828
  28. package/dist/nextcloud-vue.cjs.js.map +2 -2
  29. package/dist/nextcloud-vue.css +269 -74
  30. package/package.json +1 -1
  31. package/src/components/CnDashboardPage/CnDashboardPage.vue +3 -113
  32. package/src/components/CnDataTable/CnDataTable.vue +40 -1
  33. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  34. package/src/components/CnDetailPage/CnDetailPage.vue +237 -8
  35. package/src/components/CnTileWidget/CnTileWidget.vue +30 -2
  36. package/src/css/detail-page.css +42 -0
  37. package/src/css/dialog.css +13 -0
  38. package/src/css/look-board-detail.css +1 -1
  39. package/src/css/look-board-index.css +8 -0
  40. package/src/css/look-board.css +97 -0
  41. package/src/css/table.css +12 -0
  42. package/src/mixins/pageFilters.js +138 -0
  43. package/src/schemas/app-manifest-v2.schema.json +41 -3
  44. package/src/utils/actionLink.js +9 -3
  45. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -1,7 +1,7 @@
1
1
  import { getCurrentUser } from '@nextcloud/auth';
2
2
  import { subscribe, unsubscribe } from '@nextcloud/event-bus';
3
3
  import { translate } from '@nextcloud/l10n';
4
- import { NcActionButton, NcActionCaption, NcActionLink, NcActionSeparator, NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue';
4
+ import { NcActionButton, NcActionCaption, NcActionLink, NcActionSeparator, NcButton, NcEmptyContent, NcLoadingIcon, NcSelect } from '@nextcloud/vue';
5
5
  import { computed, ref, provide, watch, markRaw } from 'vue';
6
6
  import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue';
7
7
  import ArrowLeft from 'vue-material-design-icons/ArrowLeft.vue';
@@ -45,8 +45,9 @@ import { useObjectPresence } from '../../composables/useObjectPresence.js';
45
45
  import { useObjectSubscription } from '../../composables/useObjectSubscription.js';
46
46
  import { createRefLabelResolver } from '../../composables/useRefLabels.js';
47
47
  import { gridLayout } from '../../mixins/gridLayout.js';
48
+ import { pageFilters } from '../../mixins/pageFilters.js';
48
49
  import { pageViews } from '../../mixins/pageViews.js';
49
- import { headerActionAsLink } from '../../utils/actionLink.js';
50
+ import { resolveManifestHref, routeHref, headerActionAsLink } from '../../utils/actionLink.js';
50
51
  import { isAppInstalled } from '../../utils/appInstalled.js';
51
52
  import { compactLayoutRows } from '../../utils/dashboardPlacement.js';
52
53
  import { defaultDetailGrid } from '../../utils/defaultDetailGrid.js';
@@ -58,6 +59,8 @@ import { firstFilled } from '../../utils/fillTemplate.js';
58
59
  import { cnGridCellStyle, hasGridRow } from '../../utils/grid.js';
59
60
  import { resolveHeaderMeta, isActivityWidget } from '../../utils/headerMeta.js';
60
61
  import { prefixUrl } from '../../utils/headers.js';
62
+ import { interpolateUrlTokens } from '../../utils/interpolateUrlTokens.js';
63
+ import { followLinkClick } from '../../utils/linkNavigation.js';
61
64
  import { patchStoredSelf } from '../../utils/patchStoredSelf.js';
62
65
  import { setReadState } from '../../utils/recordInteractions.js';
63
66
  import { saveEdited } from '../../utils/saveEdited.js';
@@ -266,6 +269,7 @@ var script = {
266
269
  CnFormDialog: __sfc_main$8,
267
270
  CnDashboardGrid: __sfc_main$9,
268
271
  CnSegmentedControl: __sfc_main$a,
272
+ NcSelect,
269
273
  ViewDashboardOutline,
270
274
  CnLifecycleActions: __sfc_main$b,
271
275
  CnActionButtons: __sfc_main$c,
@@ -288,7 +292,7 @@ var script = {
288
292
  Restore,
289
293
  },
290
294
 
291
- mixins: [gridLayout, pageViews],
295
+ mixins: [gridLayout, pageFilters, pageViews],
292
296
 
293
297
  inject: {
294
298
  /**
@@ -1481,6 +1485,39 @@ var script = {
1481
1485
  default: () => [],
1482
1486
  },
1483
1487
 
1488
+ /**
1489
+ * A row of widgets across the whole page, above the body and the side
1490
+ * column (manifest `config.band`). Each entry is the id of a widget
1491
+ * declared in `widgets`, a widget definition, or `{ widgetId, span,
1492
+ * chrome }`; `span` is the entry's share of the row in twelfths
1493
+ * (default 12) and the row wraps; `chrome` is `card` or `bare` (a
1494
+ * banner defaults to bare, anything else to card, so a widget that
1495
+ * draws its own card, such as a KPI, asks for bare). Each entry
1496
+ * follows its `visibleWhen`. Empty (the default) draws no band.
1497
+ *
1498
+ * @type {Array<string|object>}
1499
+ * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-a-band-across-the-page-above-the-body-and-the-side-column
1500
+ */
1501
+ band: {
1502
+ type: Array,
1503
+ default: () => [],
1504
+ },
1505
+
1506
+ /**
1507
+ * Page-level filters (manifest `config.pageFilters`), the same a
1508
+ * dashboard takes: each writes its choice into the page's workspace
1509
+ * context, which every widget on the page reads through
1510
+ * `@page.<key>` / `@workspace.<key>`. Drawn in the band, above its
1511
+ * widgets (PtPortal "Periode").
1512
+ *
1513
+ * @type {Array<{key: string, label?: string, type?: string, control?: ('select'|'segmented'), options?: Array<{value: (string|number), label: string}>, optionsFrom?: object, clearable?: boolean, placeholder?: string, default?: (string|number)}>}
1514
+ * @spec openspec/changes/detail-page-filters/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-page-filters-that-its-widgets-read
1515
+ */
1516
+ pageFilters: {
1517
+ type: Array,
1518
+ default: () => [],
1519
+ },
1520
+
1484
1521
  /**
1485
1522
  * A field path whose value picks the side column and its order for
1486
1523
  * this record (manifest `config.sideColumnFrom`), read like a
@@ -1813,10 +1850,16 @@ var script = {
1813
1850
  // which no register answers.
1814
1851
  { schemaSlug: () => props.schema || sidebarSchema() },
1815
1852
  );
1853
+ // The page's workspace context, as a dashboard provides it: page
1854
+ // filters write their choice here and a widget reads it through
1855
+ // `@page.<key>` / `@workspace.<key>`.
1856
+ const workspaceContext = ref({});
1857
+ provide('cnWorkspaceContext', workspaceContext);
1816
1858
  return {
1817
1859
  ...registryExposed,
1818
1860
  subscriptionStatus: subscription.status,
1819
1861
  lockState: lock,
1862
+ workspaceContext,
1820
1863
  }
1821
1864
  },
1822
1865
 
@@ -2249,7 +2292,46 @@ var script = {
2249
2292
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
2250
2293
  */
2251
2294
  primaryActionHref() {
2252
- return this.stagePrimaryEntry?.href || ''
2295
+ if (this.stagePrimaryEntry) {
2296
+ return this.stagePrimaryEntry.href || ''
2297
+ }
2298
+ return this.primaryActionLink?.href || ''
2299
+ },
2300
+
2301
+ /**
2302
+ * The page's `primaryAction` as a link, when no stage action stands in
2303
+ * for it: `href` (its record tokens resolved) opens in a new tab, as the
2304
+ * schema says; `route` (with `query`) is an in-app link a plain click
2305
+ * routes to (PtPortal "Nieuwe pagina" opens the page create flow).
2306
+ *
2307
+ * @return {{href: string, to: object|null, target: string}|null} The link, or null.
2308
+ * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-pages-own-primary-action-is-a-link
2309
+ */
2310
+ primaryActionLink() {
2311
+ const action = this.primaryAction;
2312
+ if (!action || typeof action !== 'object') {
2313
+ return null
2314
+ }
2315
+ if (typeof action.href === 'string' && action.href !== '') {
2316
+ const href = resolveManifestHref(interpolateUrlTokens(action.href, this.headerLinkTokenCtx));
2317
+ return href ? { href, to: null, target: '_blank' } : null
2318
+ }
2319
+ if (typeof action.route === 'string' && action.route !== '') {
2320
+ const to = { name: action.route, ...(action.query && typeof action.query === 'object' ? { query: action.query } : {}) };
2321
+ const href = routeHref(to, this.$router);
2322
+ return href ? { href, to, target: '_self' } : null
2323
+ }
2324
+ return null
2325
+ },
2326
+
2327
+ /**
2328
+ * The token context a header link resolves its URL against: this record.
2329
+ *
2330
+ * @return {{objectId: (string|number), object: object}} The context.
2331
+ * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-header-link-builds-its-url-from-the-record
2332
+ */
2333
+ headerLinkTokenCtx() {
2334
+ return { objectId: this.objectId, object: this.resolvedObject || {} }
2253
2335
  },
2254
2336
 
2255
2337
  /**
@@ -2259,7 +2341,10 @@ var script = {
2259
2341
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
2260
2342
  */
2261
2343
  primaryActionTarget() {
2262
- return this.stagePrimaryEntry?.linkTarget || undefined
2344
+ if (this.stagePrimaryEntry) {
2345
+ return this.stagePrimaryEntry.linkTarget || undefined
2346
+ }
2347
+ return this.primaryActionLink?.target || undefined
2263
2348
  },
2264
2349
 
2265
2350
  /**
@@ -2655,6 +2740,57 @@ var script = {
2655
2740
  * @return {boolean} True when it has widgets and the body is showing.
2656
2741
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-side-column
2657
2742
  */
2743
+ /**
2744
+ * The band's widgets, resolved and visible, each with its span.
2745
+ *
2746
+ * @return {Array<{widget: object, span: number, chrome: string}>}
2747
+ * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-a-band-across-the-page-above-the-body-and-the-side-column
2748
+ */
2749
+ bandWidgets() {
2750
+ const declared = Array.isArray(this.widgets) ? this.widgets : [];
2751
+ return (Array.isArray(this.band) ? this.band : [])
2752
+ .map((entry, index) => {
2753
+ let widget = null;
2754
+ let span = 12;
2755
+ if (typeof entry === 'string') {
2756
+ widget = declared.find((candidate) => candidate && candidate.id === entry) || null;
2757
+ } else if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
2758
+ span = Number.isInteger(entry.span) ? Math.min(12, Math.max(1, entry.span)) : 12;
2759
+ if (typeof entry.widgetId === 'string') {
2760
+ widget = declared.find((candidate) => candidate && candidate.id === entry.widgetId) || null;
2761
+ } else if (typeof entry.type === 'string') {
2762
+ widget = entry.id ? entry : { ...entry, id: `cn-band-${index}` };
2763
+ }
2764
+ }
2765
+ if (!widget) {
2766
+ return null
2767
+ }
2768
+ const chrome = ['card', 'bare'].includes(entry?.chrome) ? entry.chrome : (widget.type === 'banner' ? 'bare' : 'card');
2769
+ return { widget, span, chrome }
2770
+ })
2771
+ .filter((entry) => entry && this.sideWidgetVisible(entry.widget))
2772
+ },
2773
+
2774
+ /**
2775
+ * Whether the band renders: it has a visible widget and the record shows.
2776
+ *
2777
+ * @return {boolean}
2778
+ * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-a-band-across-the-page-above-the-body-and-the-side-column
2779
+ */
2780
+ showsBand() {
2781
+ return (this.bandWidgets.length > 0 || this.detailPageFilters.length > 0) && this.showsBody
2782
+ },
2783
+
2784
+ /**
2785
+ * The declared page filters that carry a key.
2786
+ *
2787
+ * @return {Array<object>}
2788
+ * @spec openspec/changes/detail-page-filters/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-page-filters-that-its-widgets-read
2789
+ */
2790
+ detailPageFilters() {
2791
+ return (Array.isArray(this.pageFilters) ? this.pageFilters : []).filter((pf) => pf && pf.key)
2792
+ },
2793
+
2658
2794
  showsSideColumn() {
2659
2795
  return this.sideColumnWidgets.length > 0 && this.showsBody
2660
2796
  },
@@ -3020,7 +3156,7 @@ var script = {
3020
3156
  .map((pinned) => pinned.inline);
3021
3157
  const combined = inline.length > 0 ? [...authored, ...inline] : authored;
3022
3158
  // An entry (or child) with `href` becomes a link CnActionButtons renders as an anchor.
3023
- const linked = combined.map(headerActionAsLink);
3159
+ const linked = combined.map((action) => headerActionAsLink(action, this.headerLinkTokenCtx));
3024
3160
  const declared = linked.some((action, index) => action !== combined[index]) ? linked : combined;
3025
3161
  if (!(this.foldsEditIntoActions && this.canEditRecord)) {
3026
3162
  return declared
@@ -4120,6 +4256,10 @@ var script = {
4120
4256
 
4121
4257
  created() {
4122
4258
  this.pushAiContext();
4259
+ // Seeded before the first render, so a widget's first request already
4260
+ // carries the page filter's value.
4261
+ this.initPageFilters();
4262
+ this.loadPageFilterOptions();
4123
4263
  },
4124
4264
 
4125
4265
  mounted() {
@@ -4362,16 +4502,21 @@ var script = {
4362
4502
  * the `run()` CnActionButtons bound for it, and announced to the host
4363
4503
  * with its declaration. A link action is left to the browser and
4364
4504
  * announces nothing. Without a stage action this is the page's
4365
- * `primaryAction`, announced as before.
4505
+ * `primaryAction`, announced as before, and when it is an in-app link
4506
+ * a plain click routes there.
4366
4507
  *
4508
+ * @param {MouseEvent} [event] The click.
4367
4509
  * @return {void}
4368
4510
  * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage
4369
4511
  * @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
4370
4512
  */
4371
- onPrimaryActionClick() {
4513
+ onPrimaryActionClick(event) {
4372
4514
  const entry = this.stagePrimaryEntry;
4373
4515
  if (!entry) {
4374
4516
  this.$emit('primary-action', this.primaryAction);
4517
+ if (this.primaryActionLink?.to) {
4518
+ followLinkClick(event, this.primaryActionLink.to, this.$router);
4519
+ }
4375
4520
  return
4376
4521
  }
4377
4522
  if (entry.href) {