@conduction/nextcloud-vue 0.1.0-beta.14 → 0.1.0-beta.16

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 (46) hide show
  1. package/dist/nextcloud-vue.cjs.js +7282 -3443
  2. package/dist/nextcloud-vue.cjs.js.map +1 -1
  3. package/dist/nextcloud-vue.css +719 -100
  4. package/dist/nextcloud-vue.esm.js +7120 -3300
  5. package/dist/nextcloud-vue.esm.js.map +1 -1
  6. package/package.json +3 -2
  7. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +36 -3
  8. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +34 -19
  9. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +312 -36
  10. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +983 -64
  11. package/src/components/CnAdvancedFormDialog/index.js +3 -0
  12. package/src/components/CnAppLoading/CnAppLoading.vue +93 -0
  13. package/src/components/CnAppLoading/index.js +3 -0
  14. package/src/components/CnAppNav/CnAppNav.vue +269 -0
  15. package/src/components/CnAppNav/index.js +3 -0
  16. package/src/components/CnAppRoot/CnAppRoot.vue +201 -0
  17. package/src/components/CnAppRoot/index.js +3 -0
  18. package/src/components/CnColorPicker/CnColorPicker.vue +251 -0
  19. package/src/components/CnColorPicker/index.js +1 -0
  20. package/src/components/CnContextMenu/CnContextMenu.vue +41 -4
  21. package/src/components/CnDashboardPage/CnDashboardPage.vue +8 -0
  22. package/src/components/CnDependencyMissing/CnDependencyMissing.vue +152 -0
  23. package/src/components/CnDependencyMissing/index.js +3 -0
  24. package/src/components/CnDetailPage/CnDetailPage.vue +27 -16
  25. package/src/components/CnIndexPage/CnIndexPage.vue +36 -6
  26. package/src/components/CnPageRenderer/CnPageRenderer.vue +278 -0
  27. package/src/components/CnPageRenderer/index.js +4 -0
  28. package/src/components/CnPageRenderer/pageTypes.js +37 -0
  29. package/src/components/CnRowActions/CnRowActions.vue +44 -3
  30. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +4 -0
  31. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +103 -74
  32. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +30 -2
  33. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +16 -12
  34. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +9 -4
  35. package/src/components/index.js +7 -1
  36. package/src/composables/index.js +2 -0
  37. package/src/composables/useAppManifest.js +115 -0
  38. package/src/composables/useAppStatus.js +107 -0
  39. package/src/css/CnSchemaFormDialog.css +22 -0
  40. package/src/index.js +24 -2
  41. package/src/schemas/app-manifest.schema.json +153 -0
  42. package/src/types/index.d.ts +9 -0
  43. package/src/types/manifest.d.ts +88 -0
  44. package/src/utils/index.js +1 -1
  45. package/src/utils/schema.js +157 -2
  46. package/src/utils/validateManifest.js +113 -0
@@ -0,0 +1,251 @@
1
+ <template>
2
+ <NcPopover
3
+ :shown.sync="open"
4
+ :disabled="disabled"
5
+ :triggers="[]"
6
+ popup-role="dialog"
7
+ popover-base-class="cn-color-picker__popper">
8
+ <template #trigger>
9
+ <button
10
+ type="button"
11
+ class="cn-color-picker__swatch"
12
+ :class="{ 'cn-color-picker__swatch--disabled': disabled }"
13
+ :style="swatchStyle"
14
+ :disabled="disabled"
15
+ :title="t('nextcloud-vue', 'Open color picker')"
16
+ :aria-label="t('nextcloud-vue', 'Open color picker')"
17
+ @click="open = !open" />
18
+ </template>
19
+ <ChromeColorPicker
20
+ ref="picker"
21
+ v-bind="$attrs"
22
+ class="cn-color-picker__chrome"
23
+ :class="{ 'cn-color-picker__chrome--locked-mode': mode !== null }"
24
+ :value="value"
25
+ v-on="$listeners" />
26
+ </NcPopover>
27
+ </template>
28
+
29
+ <script>
30
+ import { translate as t } from '@nextcloud/l10n'
31
+ import { NcPopover } from '@nextcloud/vue'
32
+ import { Chrome as ChromeColorPicker } from 'vue-color'
33
+
34
+ /**
35
+ * CnColorPicker — A swatch-button trigger that opens a themed `Chrome` color
36
+ * picker (vue-color) inside a popover.
37
+ *
38
+ * The active color is shown as a square swatch (with a checker pattern behind
39
+ * it so alpha colors render correctly). Clicking the swatch toggles the
40
+ * popover. All props and listeners except `value` and `disabled` are
41
+ * forwarded to the underlying `Chrome` picker, so the full vue-color API
42
+ * stays available (`disable-alpha`, `disable-fields`, `@input`, etc.).
43
+ *
44
+ * The `value` prop accepts any of vue-color's color formats: a CSS color
45
+ * string (`'#abcdef'`, `'rgba(...)'`, ...) or a color object.
46
+ *
47
+ * @event input Forwarded from `Chrome`. Payload: vue-color color object
48
+ * `{ hex, hex8, rgba, hsl, hsv, a, source }`.
49
+ */
50
+ export default {
51
+ name: 'CnColorPicker',
52
+
53
+ components: {
54
+ NcPopover,
55
+ ChromeColorPicker,
56
+ },
57
+
58
+ inheritAttrs: false,
59
+
60
+ props: {
61
+ /**
62
+ * Current color. Accepts any of vue-color's input formats: CSS color
63
+ * string or color object. `null`/empty renders a transparent swatch.
64
+ */
65
+ value: {
66
+ type: [String, Object],
67
+ default: null,
68
+ },
69
+ /** Disables the swatch trigger and prevents the popover from opening. */
70
+ disabled: {
71
+ type: Boolean,
72
+ default: false,
73
+ },
74
+ /**
75
+ * Lock the picker's numeric input fields to a single mode and hide the
76
+ * mode-toggle button. One of `'hex'`, `'rgb'`, `'hsl'`. When `null`
77
+ * (default) the user can switch modes themselves. The actual fields
78
+ * shown for `'rgb'`/`'hsl'` include alpha when `disable-alpha` is
79
+ * `false` (i.e. they become RGBA / HSLA).
80
+ */
81
+ mode: {
82
+ type: String,
83
+ default: null,
84
+ validator: (v) => v === null || ['hex', 'rgb', 'hsl'].includes(v),
85
+ },
86
+ },
87
+
88
+ data() {
89
+ return {
90
+ open: false,
91
+ }
92
+ },
93
+
94
+ computed: {
95
+ /**
96
+ * CSS background for the swatch. Layers a solid color over a checker
97
+ * pattern so alpha values are visible. Falls back to just the checker
98
+ * when no value is set.
99
+ */
100
+ swatchStyle() {
101
+ const c = typeof this.value === 'string'
102
+ ? this.value
103
+ : (this.value?.hex8 || this.value?.hex)
104
+ if (!c) return {}
105
+ // Layer the solid fill on top of the four-gradient checker. Each
106
+ // checker layer needs its own offset so the squares alternate; if
107
+ // they all share `0 0` the pattern collapses to a single square.
108
+ const fill = `linear-gradient(${c}, ${c})`
109
+ return {
110
+ backgroundImage: `${fill}, var(--cn-color-picker-checker)`,
111
+ backgroundSize: '100% 100%, 8px 8px, 8px 8px, 8px 8px, 8px 8px',
112
+ backgroundPosition: '0 0, 0 0, 0 4px, 4px -4px, -4px 0',
113
+ }
114
+ },
115
+ },
116
+
117
+ watch: {
118
+ mode: {
119
+ immediate: true,
120
+ handler() {
121
+ // Wait for the picker ref to exist (initial mount + when the
122
+ // popover first renders the picker).
123
+ this.$nextTick(() => this.applyMode())
124
+ },
125
+ },
126
+ open(isOpen) {
127
+ // Re-apply on every open: vue-color resets `fieldsIndex` if the
128
+ // component is unmounted/remounted by the popover.
129
+ if (isOpen) this.$nextTick(() => this.applyMode())
130
+ },
131
+ },
132
+
133
+ methods: {
134
+ t,
135
+
136
+ /** Pin the Chrome picker's `fieldsIndex` to the requested mode. */
137
+ applyMode() {
138
+ if (!this.mode) return
139
+ const idx = { hex: 0, rgb: 1, hsl: 2 }[this.mode]
140
+ const picker = this.$refs.picker
141
+ if (picker && picker.fieldsIndex !== idx) {
142
+ picker.fieldsIndex = idx
143
+ }
144
+ },
145
+ },
146
+ }
147
+ </script>
148
+
149
+ <style scoped>
150
+ .cn-color-picker__swatch {
151
+ --cn-color-picker-checker:
152
+ linear-gradient(45deg, var(--color-background-dark) 25%, transparent 25%),
153
+ linear-gradient(-45deg, var(--color-background-dark) 25%, transparent 25%),
154
+ linear-gradient(45deg, transparent 75%, var(--color-background-dark) 75%),
155
+ linear-gradient(-45deg, transparent 75%, var(--color-background-dark) 75%);
156
+ display: inline-block;
157
+ width: 32px;
158
+ height: 32px;
159
+ flex-shrink: 0;
160
+ padding: 0;
161
+ border: 1px solid var(--color-border);
162
+ border-radius: var(--border-radius);
163
+ cursor: pointer;
164
+ background-image: var(--cn-color-picker-checker);
165
+ background-size: 8px 8px;
166
+ background-position: 0 0, 0 4px, 4px -4px, -4px 0;
167
+ overflow: hidden;
168
+ position: relative;
169
+ }
170
+
171
+ .cn-color-picker__swatch:focus-visible {
172
+ outline: 2px solid var(--color-primary-element);
173
+ outline-offset: 2px;
174
+ }
175
+
176
+ .cn-color-picker__swatch--disabled {
177
+ cursor: not-allowed;
178
+ opacity: 0.6;
179
+ }
180
+
181
+ /* Strip Chrome's hardcoded light-mode palette so it adopts the active theme.
182
+ The class lands on the picker's root via Vue's class merging, so target it
183
+ directly — NOT as a descendant. */
184
+ .cn-color-picker__chrome {
185
+ background: var(--color-main-background);
186
+ background-color: var(--color-main-background);
187
+ box-shadow: none;
188
+ color: var(--color-main-text);
189
+ font-family: var(--font-face);
190
+ overflow: hidden;
191
+ }
192
+
193
+ .cn-color-picker__chrome :deep(.vc-chrome-body),
194
+ .cn-color-picker__chrome :deep(.vc-chrome-saturation-wrap),
195
+ .cn-color-picker__chrome :deep(.vc-chrome-color-wrap),
196
+ .cn-color-picker__chrome :deep(.vc-chrome-active-color) {
197
+ background-color: var(--color-main-background);
198
+ }
199
+
200
+ .cn-color-picker__chrome :deep(.vc-chrome-toggle-icon-highlight) {
201
+ background: var(--color-background-hover);
202
+ }
203
+
204
+ .cn-color-picker__chrome :deep(.vc-hue-picker),
205
+ .cn-color-picker__chrome :deep(.vc-alpha-picker) {
206
+ background-color: var(--color-main-background);
207
+ box-shadow: 0 1px 4px 0 var(--color-box-shadow);
208
+ }
209
+
210
+ .cn-color-picker__chrome :deep(.vc-chrome-fields .vc-input__input) {
211
+ background-color: var(--color-main-background);
212
+ color: var(--color-main-text);
213
+ box-shadow: inset 0 0 0 1px var(--color-border);
214
+ }
215
+
216
+ .cn-color-picker__chrome :deep(.vc-chrome-fields .vc-input__label) {
217
+ color: var(--color-text-maxcontrast);
218
+ }
219
+
220
+ .cn-color-picker__chrome :deep(.vc-chrome-toggle-icon svg path) {
221
+ fill: var(--color-main-text);
222
+ }
223
+
224
+ /* When `mode` is set, lock the field-mode toggle so the user can't switch
225
+ between hex/rgb/hsl. */
226
+ .cn-color-picker__chrome--locked-mode :deep(.vc-chrome-toggle-btn) {
227
+ display: none;
228
+ }
229
+ </style>
230
+
231
+ <!-- Non-scoped overrides: NcPopover renders into a portal at document.body,
232
+ so scoped styles can't reach it. Targeted by `popoverBaseClass`. -->
233
+ <style>
234
+ .cn-color-picker__popper .v-popper__inner,
235
+ .cn-color-picker__popper .v-popper__wrapper {
236
+ background: var(--color-main-background);
237
+ background-color: var(--color-main-background);
238
+ color: var(--color-main-text);
239
+ }
240
+
241
+ .cn-color-picker__popper .v-popper__arrow-inner,
242
+ .cn-color-picker__popper .v-popper__arrow-outer {
243
+ border-color: var(--color-main-background);
244
+ }
245
+
246
+ /* Default NcPopover style sets `top: -9px` with specificity (0,4,1) using a
247
+ hashed class name; bump with !important rather than depend on the hash. */
248
+ .cn-color-picker__popper .v-popper__arrow-container {
249
+ top: -10px !important;
250
+ }
251
+ </style>
@@ -0,0 +1 @@
1
+ export { default as CnColorPicker } from './CnColorPicker.vue'
@@ -8,8 +8,9 @@
8
8
  @close="onClose">
9
9
  <!-- Dynamic actions from array prop -->
10
10
  <NcActionButton
11
- v-for="action in actions"
11
+ v-for="action in visibleActions"
12
12
  :key="action.label"
13
+ :title="resolveTitle(action)"
13
14
  :disabled="resolveDisabled(action)"
14
15
  :class="{ 'cn-row-action--destructive': action.destructive }"
15
16
  close-after-click
@@ -77,9 +78,13 @@ export default {
77
78
  },
78
79
  /**
79
80
  * Action definitions rendered as NcActionButton items.
80
- * Same format as CnRowActions: `{ label, icon?, handler?, disabled?, destructive? }`.
81
- * When empty, only the default slot content is rendered.
82
- * @type {Array<{label: string, icon?: object, handler?: Function, disabled?: boolean | Function, destructive?: boolean}>}
81
+ * Same format as CnRowActions: `{ label, icon?, handler?, disabled?, visible?, title?, destructive? }`.
82
+ * `visible` (boolean | (targetItem) => boolean) hides the entry when falsy
83
+ * (default: shown). `title` (string | (targetItem) => string) renders as
84
+ * a native tooltip — useful for explaining why an entry is disabled.
85
+ * When the entire array is empty (or all entries are filtered out), only
86
+ * the default slot content is rendered.
87
+ * @type {Array<{label: string, icon?: object, handler?: Function, disabled?: boolean | Function, visible?: boolean | Function, title?: string | Function, destructive?: boolean}>}
83
88
  */
84
89
  actions: {
85
90
  type: Array,
@@ -102,6 +107,23 @@ export default {
102
107
  }
103
108
  },
104
109
 
110
+ computed: {
111
+ /**
112
+ * Filter actions by their `visible` predicate. Entries without
113
+ * `visible` are always shown (backwards compatible).
114
+ * @return {Array} Visible actions for the current targetItem.
115
+ */
116
+ visibleActions() {
117
+ return this.actions.filter((action) => {
118
+ if (action.visible === undefined) return true
119
+ if (typeof action.visible === 'function') {
120
+ return !!action.visible(this.targetItem)
121
+ }
122
+ return !!action.visible
123
+ })
124
+ },
125
+ },
126
+
105
127
  watch: {
106
128
  open(val) {
107
129
  this.internalOpen = val
@@ -119,6 +141,21 @@ export default {
119
141
  return !!action.disabled
120
142
  },
121
143
 
144
+ /**
145
+ * Resolve the `title` field on an action descriptor — supports both
146
+ * a static string and a function `(targetItem) => string`. Returns
147
+ * undefined when no title is provided so the attribute isn't rendered.
148
+ *
149
+ * @param {object} action The action descriptor.
150
+ * @return {string|undefined} The tooltip text, or undefined.
151
+ */
152
+ resolveTitle(action) {
153
+ if (typeof action.title === 'function') {
154
+ return action.title(this.targetItem) || undefined
155
+ }
156
+ return action.title || undefined
157
+ },
158
+
122
159
  onAction(action) {
123
160
  if (action.handler && typeof action.handler === 'function') {
124
161
  action.handler(this.targetItem)
@@ -22,6 +22,14 @@
22
22
  </p>
23
23
  </div>
24
24
  <div class="cn-dashboard-page__header-actions">
25
+ <!-- Public slot. Documented in CLAUDE.md and used by every
26
+ existing consumer (decidesk, mydash, opencatalogi,
27
+ pipelinq, procest). -->
28
+ <slot name="header-actions" />
29
+ <!-- Back-compat alias: original slot name shipped before
30
+ CLAUDE.md was updated. Render alongside so any
31
+ stragglers still work; consumers should prefer
32
+ #header-actions. -->
25
33
  <slot name="actions" />
26
34
  <NcButton
27
35
  v-if="allowEdit"
@@ -0,0 +1,152 @@
1
+ <!--
2
+ CnDependencyMissing — full-page screen shown when one or more apps
3
+ declared in `manifest.dependencies` are not installed or not enabled.
4
+
5
+ CnAppRoot mounts this in its dependency-check phase (between loading
6
+ and shell). Apps can override CnAppRoot's #dependency-missing slot to
7
+ customise the screen.
8
+
9
+ See REQ-JMR-011 of the json-manifest-renderer specification.
10
+ -->
11
+ <template>
12
+ <div class="cn-dependency-missing">
13
+ <div class="cn-dependency-missing__inner">
14
+ <h1 class="cn-dependency-missing__heading">
15
+ {{ heading }}
16
+ </h1>
17
+ <p class="cn-dependency-missing__intro">
18
+ {{ intro }}
19
+ </p>
20
+ <ul class="cn-dependency-missing__list">
21
+ <li
22
+ v-for="dep in dependencies"
23
+ :key="dep.id"
24
+ class="cn-dependency-missing__item">
25
+ <span class="cn-dependency-missing__item-name">{{ dep.name || dep.id }}</span>
26
+ <a
27
+ class="cn-dependency-missing__item-link"
28
+ :href="resolveLink(dep)"
29
+ target="_self">
30
+ {{ dep.enabled === false ? enableLabel : installLabel }}
31
+ </a>
32
+ </li>
33
+ </ul>
34
+ </div>
35
+ </div>
36
+ </template>
37
+
38
+ <script>
39
+ export default {
40
+ name: 'CnDependencyMissing',
41
+
42
+ props: {
43
+ /**
44
+ * Array of missing dependencies. Each entry:
45
+ * { id, name?, installUrl?, enabled? }
46
+ * - `id` is the Nextcloud app id (matches the entries in
47
+ * manifest.dependencies)
48
+ * - `name` is a human-readable label; `id` is used as a fallback
49
+ * - `installUrl` overrides the default install/enable URL when
50
+ * set; otherwise the default Nextcloud apps page is used
51
+ * - `enabled` discriminates the link label: `false` means the
52
+ * app is installed but disabled; otherwise it's not installed
53
+ *
54
+ * @type {Array<{id: string, name?: string, installUrl?: string, enabled?: boolean}>}
55
+ */
56
+ dependencies: {
57
+ type: Array,
58
+ required: true,
59
+ },
60
+ /**
61
+ * Optional name of the host app, included in the default heading.
62
+ *
63
+ * @type {string}
64
+ */
65
+ appName: {
66
+ type: String,
67
+ default: '',
68
+ },
69
+ /** Heading text. Override for localisation. */
70
+ heading: {
71
+ type: String,
72
+ default: 'Required apps are missing',
73
+ },
74
+ /** Introductory text under the heading. */
75
+ intro: {
76
+ type: String,
77
+ default: 'This app needs the following Nextcloud apps to be installed and enabled.',
78
+ },
79
+ /** Label for the install link. */
80
+ installLabel: {
81
+ type: String,
82
+ default: 'Install',
83
+ },
84
+ /** Label for the enable link (used when dep.enabled === false). */
85
+ enableLabel: {
86
+ type: String,
87
+ default: 'Enable',
88
+ },
89
+ },
90
+
91
+ methods: {
92
+ resolveLink(dep) {
93
+ if (dep.installUrl) return dep.installUrl
94
+ return '/index.php/settings/apps'
95
+ },
96
+ },
97
+ }
98
+ </script>
99
+
100
+ <style>
101
+ .cn-dependency-missing {
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ width: 100%;
106
+ min-height: 100vh;
107
+ background: var(--color-main-background);
108
+ color: var(--color-main-text);
109
+ }
110
+
111
+ .cn-dependency-missing__inner {
112
+ max-width: 600px;
113
+ padding: calc(4 * var(--default-grid-baseline));
114
+ }
115
+
116
+ .cn-dependency-missing__heading {
117
+ margin: 0 0 calc(2 * var(--default-grid-baseline));
118
+ font-size: 1.5em;
119
+ }
120
+
121
+ .cn-dependency-missing__intro {
122
+ margin: 0 0 calc(3 * var(--default-grid-baseline));
123
+ color: var(--color-text-maxcontrast);
124
+ }
125
+
126
+ .cn-dependency-missing__list {
127
+ margin: 0;
128
+ padding: 0;
129
+ list-style: none;
130
+ }
131
+
132
+ .cn-dependency-missing__item {
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: space-between;
136
+ padding: calc(2 * var(--default-grid-baseline)) 0;
137
+ border-bottom: 1px solid var(--color-border);
138
+ }
139
+
140
+ .cn-dependency-missing__item:last-child {
141
+ border-bottom: 0;
142
+ }
143
+
144
+ .cn-dependency-missing__item-name {
145
+ font-weight: bold;
146
+ }
147
+
148
+ .cn-dependency-missing__item-link {
149
+ color: var(--color-primary-element);
150
+ text-decoration: underline;
151
+ }
152
+ </style>
@@ -0,0 +1,3 @@
1
+ import CnDependencyMissing from './CnDependencyMissing.vue'
2
+ export default CnDependencyMissing
3
+ export { CnDependencyMissing }
@@ -24,23 +24,34 @@
24
24
  <div class="cn-detail-page" :style="{ maxWidth: maxWidth }">
25
25
  <!-- Header -->
26
26
  <div class="cn-detail-page__header">
27
- <div class="cn-detail-page__header-left">
28
- <slot name="icon">
29
- <CnIcon
30
- v-if="icon"
31
- :name="icon"
32
- :size="iconSize"
33
- class="cn-detail-page__icon" />
34
- </slot>
35
- <div class="cn-detail-page__header-text">
36
- <h2 v-if="title" class="cn-detail-page__title">
37
- {{ title }}
38
- </h2>
39
- <p v-if="description" class="cn-detail-page__description">
40
- {{ description }}
41
- </p>
27
+ <!-- Header (left block) — overridable via #header slot. Default
28
+ renders the icon + title + description. The right-hand
29
+ #actions slot remains separate so headerComponent and
30
+ actionsComponent can be replaced independently. -->
31
+ <slot
32
+ name="header"
33
+ :title="title"
34
+ :description="description"
35
+ :icon="icon"
36
+ :icon-size="iconSize">
37
+ <div class="cn-detail-page__header-left">
38
+ <slot name="icon">
39
+ <CnIcon
40
+ v-if="icon"
41
+ :name="icon"
42
+ :size="iconSize"
43
+ class="cn-detail-page__icon" />
44
+ </slot>
45
+ <div class="cn-detail-page__header-text">
46
+ <h2 v-if="title" class="cn-detail-page__title">
47
+ {{ title }}
48
+ </h2>
49
+ <p v-if="description" class="cn-detail-page__description">
50
+ {{ description }}
51
+ </p>
52
+ </div>
42
53
  </div>
43
- </div>
54
+ </slot>
44
55
  <div class="cn-detail-page__header-actions">
45
56
  <slot name="actions" />
46
57
  </div>
@@ -1,11 +1,19 @@
1
1
  <template>
2
2
  <div class="cn-index-page">
3
- <!-- Header (hidden by default shown in sidebar instead) -->
4
- <CnPageHeader
5
- v-if="showTitle"
3
+ <!-- Header overridable via #header slot. Default renders CnPageHeader
4
+ when showTitle is true (existing behaviour, hidden by default). -->
5
+ <slot
6
+ name="header"
6
7
  :title="title"
7
8
  :description="description"
8
- :icon="resolvedIcon" />
9
+ :icon="resolvedIcon"
10
+ :show-title="showTitle">
11
+ <CnPageHeader
12
+ v-if="showTitle"
13
+ :title="title"
14
+ :description="description"
15
+ :icon="resolvedIcon" />
16
+ </slot>
9
17
 
10
18
  <!-- Optional content below header, above actions bar -->
11
19
  <div v-if="$scopedSlots['below-header']" class="cn-index-page__below-header">
@@ -548,6 +556,17 @@ export default {
548
556
  type: Boolean,
549
557
  default: false,
550
558
  },
559
+ /**
560
+ * Whether to add a View action to row actions. The action emits a
561
+ * dedicated `view` event — independent of `row-click`. Bind `@view`
562
+ * to handle "open detail" and `@row-click` to handle row click
563
+ * (selection, expand, etc.); they may share a handler when the app
564
+ * wants click-to-view, but they are conceptually distinct.
565
+ */
566
+ showViewAction: {
567
+ type: Boolean,
568
+ default: true,
569
+ },
551
570
  /** Whether to add an Edit action to row actions */
552
571
  showEditAction: {
553
572
  type: Boolean,
@@ -670,12 +689,12 @@ export default {
670
689
  /** Built-in row actions based on show*Action props */
671
690
  defaultActions() {
672
691
  const builtIn = []
673
- if (this.$listeners && this.$listeners['row-click']) {
692
+ if (this.showViewAction) {
674
693
  builtIn.push({
675
694
  label: 'View',
676
695
  icon: this.schemaIconComponent,
677
696
  handler: (row) => {
678
- this.onRowClick(row)
697
+ this.onView(row)
679
698
  },
680
699
  })
681
700
  }
@@ -765,6 +784,17 @@ export default {
765
784
  this.$emit('row-click', row)
766
785
  },
767
786
 
787
+ /**
788
+ * Handle the built-in View action — emits a dedicated `view` event.
789
+ * Kept distinct from `row-click` because the two are conceptually
790
+ * different: a row click might mean select/expand/drilldown, while
791
+ * View always means "open the detail view of this row".
792
+ * @param {object} row The row whose View action was triggered
793
+ */
794
+ onView(row) {
795
+ this.$emit('view', row)
796
+ },
797
+
768
798
  /**
769
799
  * Handle the Add button click. If the consumer listens to @add,
770
800
  * emit the event (backward compatible). Otherwise open the form dialog.