@conduction/nextcloud-vue 2.56.0-beta.43 → 2.56.0-beta.44

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.
@@ -76,7 +76,7 @@
76
76
  :active="tab.id === activeTabId">
77
77
  <div class="cn-admin-settings-shell__panel" :data-tab="tab.id">
78
78
  <!-- @slot tab-<id> The panel of the tab with that `id` in `tabs`. -->
79
- <slot :name="`tab-${tab.id}`" />
79
+ <slot v-if="!lazyTabs || tab.id === activeTabId" :name="`tab-${tab.id}`" />
80
80
  </div>
81
81
  </CnTab>
82
82
  </CnTabs>
@@ -491,6 +491,19 @@ export default {
491
491
  default: '',
492
492
  },
493
493
 
494
+ /**
495
+ * Mount a tab's `tab-<id>` slot only while that tab is open, and
496
+ * unmount it when another tab opens, so each visit mounts it afresh
497
+ * and the page does not load every tab at once. Off (the default),
498
+ * every panel stays mounted and only the open one shows.
499
+ *
500
+ * @spec openspec/changes/admin-settings-lazy-tabs/specs/settings-board-look/spec.md#requirement-the-admin-shell-can-mount-only-the-open-tab
501
+ */
502
+ lazyTabs: {
503
+ type: Boolean,
504
+ default: false,
505
+ },
506
+
494
507
  /**
495
508
  * How the `tabs` strip is drawn, as CnTabs' `variant`: `line`, the
496
509
  * folder tabs (the default), or `underline`, plain labels on a rule
@@ -421,7 +421,7 @@
421
421
  :is="registryRenderer(item)"
422
422
  :widgetId="item.widgetId"
423
423
  :content="registryWidgetBindings(item)"
424
- v-bind="{ ...registryWidgetBindings(item), ...containerBindings(item) }"
424
+ v-bind="{ ...registryWidgetProps(item), ...containerBindings(item) }"
425
425
  @header="onWidgetHeader(item, $event)" />
426
426
  </CnWidgetWrapper>
427
427
  <CnWidgetWrapper
@@ -882,7 +882,7 @@
882
882
  :is="registryRenderer(item)"
883
883
  :widgetId="item.widgetId"
884
884
  :content="registryWidgetBindings(item)"
885
- v-bind="{ ...registryWidgetBindings(item), ...containerBindings(item), ...cellChromeBindings(item) }"
885
+ v-bind="{ ...registryWidgetProps(item), ...containerBindings(item), ...cellChromeBindings(item) }"
886
886
  @header="onWidgetHeader(item, $event)" />
887
887
  </CnWidgetWrapper>
888
888
  </template>
@@ -984,6 +984,7 @@ import { pageViews } from '../../mixins/pageViews.js'
984
984
  import { mergeUserLayout, resolveUserLayoutApi } from '../../store/plugins/dashboardLayouts.js'
985
985
  import { isAppInstalled } from '../../utils/appInstalled.js'
986
986
  import { contentHeaderLink } from '../../utils/contentHeaderLink.js'
987
+ import { pickDeclaredProps } from '../../utils/declaredProps.js'
987
988
  import { userLocale } from '../../utils/userLocale.js'
988
989
  import { compareVisibleWhen, evaluateWorkspaceCondition, isWorkspaceCondition, readVisibleWhenValue } from '../../utils/visibleWhen.js'
989
990
  import { canonicalWidgetType } from '../../utils/widgetTypeAliases.js'
@@ -3642,6 +3643,23 @@ export default {
3642
3643
  return outcome ? { ...content, conditionOutcome: outcome } : content
3643
3644
  },
3644
3645
 
3646
+ /**
3647
+ * The content keys a registry widget receives as props: only the ones
3648
+ * its renderer declares. An undeclared key would otherwise fall through
3649
+ * as an attribute onto the renderer's root component and could override
3650
+ * a value the widget computes for it. The whole content still arrives
3651
+ * as `content`. A renderer that forwards its own attributes
3652
+ * (`inheritAttrs: false`) keeps receiving every key.
3653
+ *
3654
+ * @param {object} item Layout item.
3655
+ * @return {object} The bindings the renderer declares.
3656
+ * @spec openspec/changes/dashboard-declared-widget-props/specs/dashboard-page/spec.md#requirement-a-dashboard-widget-receives-only-the-content-keys-it-declares
3657
+ */
3658
+ registryWidgetProps(item) {
3659
+ const def = this.getWidgetDef(item.widgetId)
3660
+ return pickDeclaredProps(this.registryRenderer(item), this.registryWidgetBindings(item), def?.type || item.widgetId)
3661
+ },
3662
+
3645
3663
  /**
3646
3664
  * What a container widget (registry `container`, such as `tabs`) needs
3647
3665
  * beside its content: the page's widget definitions, so its tabs find
@@ -0,0 +1,115 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ /*
7
+ * `process.env.NODE_ENV` is substituted by every consuming app's bundler at
8
+ * build time; see NcSelectTags.js.
9
+ */
10
+ /* global process */
11
+
12
+ import { camelize } from 'vue'
13
+
14
+ const warned = new Set()
15
+
16
+ /**
17
+ * Add the prop names one component options object declares, following its
18
+ * `extends` and `mixins`.
19
+ *
20
+ * @param {object} options A component options object.
21
+ * @param {Set<string>} names The set to add camelCase prop names to.
22
+ * @param {number} depth Recursion guard.
23
+ */
24
+ function collect(options, names, depth) {
25
+ if (!options || typeof options !== 'object' || depth > 8) {
26
+ return
27
+ }
28
+ const props = options.props
29
+ if (Array.isArray(props)) {
30
+ props.forEach((name) => names.add(camelize(String(name))))
31
+ } else if (props && typeof props === 'object') {
32
+ Object.keys(props).forEach((name) => names.add(camelize(name)))
33
+ }
34
+ collect(options.extends, names, depth + 1)
35
+ if (Array.isArray(options.mixins)) {
36
+ options.mixins.forEach((mixin) => collect(mixin, names, depth + 1))
37
+ }
38
+ }
39
+
40
+ /**
41
+ * The prop names a component declares, or `null` when it takes every
42
+ * attribute itself and so must keep receiving all of them:
43
+ * - `inheritAttrs: false`: the component forwards `$attrs` on purpose;
44
+ * - a functional component without `props`: its attrs are its props;
45
+ * - an async component that has not resolved yet.
46
+ *
47
+ * @spec openspec/changes/dashboard-declared-widget-props/specs/dashboard-page/spec.md#requirement-a-dashboard-widget-receives-only-the-content-keys-it-declares
48
+ * @param {object|((...args: unknown[]) => unknown)|null} component A component definition.
49
+ * @return {Set<string>|null} camelCase prop names, or null for "all".
50
+ */
51
+ export function declaredPropNames(component) {
52
+ if (!component) {
53
+ return null
54
+ }
55
+ if (typeof component === 'function') {
56
+ if (!component.props) {
57
+ return null
58
+ }
59
+ const names = new Set()
60
+ collect({ props: component.props }, names, 0)
61
+ return names
62
+ }
63
+ if (typeof component !== 'object') {
64
+ return null
65
+ }
66
+ if (component.__asyncLoader) {
67
+ return component.__asyncResolved ? declaredPropNames(component.__asyncResolved) : null
68
+ }
69
+ const chain = [component, component.extends, ...(component.mixins || [])]
70
+ if (chain.some((options) => options && options.inheritAttrs === false)) {
71
+ return null
72
+ }
73
+ const names = new Set()
74
+ collect(component, names, 0)
75
+ return names
76
+ }
77
+
78
+ /**
79
+ * Keep the entries of `bindings` whose key `component` declares as a prop.
80
+ * A component for which {@link declaredPropNames} returns null gets every
81
+ * entry. A dropped key is warned about once outside production when the
82
+ * component has no `content` prop either, so the key reaches nothing.
83
+ *
84
+ * @spec openspec/changes/dashboard-declared-widget-props/specs/dashboard-page/spec.md#requirement-a-dashboard-widget-receives-only-the-content-keys-it-declares
85
+ * @param {object|((...args: unknown[]) => unknown)|null} component The component that will receive the bindings.
86
+ * @param {object} bindings The candidate bindings.
87
+ * @param {string} label Names the component in the warning (for example the widget type).
88
+ * @return {object} The bindings the component declares.
89
+ */
90
+ export function pickDeclaredProps(component, bindings, label) {
91
+ const declared = declaredPropNames(component)
92
+ if (!declared || !bindings || typeof bindings !== 'object') {
93
+ return bindings
94
+ }
95
+ const out = {}
96
+ for (const [key, value] of Object.entries(bindings)) {
97
+ if (declared.has(camelize(key))) {
98
+ out[key] = value
99
+ continue
100
+ }
101
+ if (process.env.NODE_ENV !== 'production' && !declared.has('content') && !warned.has(`${label}\u0000${key}`)) {
102
+ warned.add(`${label}\u0000${key}`)
103
+ // eslint-disable-next-line no-console
104
+ console.warn(`[nextcloud-vue] content key "${key}" is not passed to widget "${label}": it declares neither that prop nor \`content\`.`)
105
+ }
106
+ }
107
+ return out
108
+ }
109
+
110
+ /**
111
+ * Forget which keys were warned about. For tests.
112
+ */
113
+ export function resetDeclaredPropWarnings() {
114
+ warned.clear()
115
+ }