@conduction/nextcloud-vue 0.1.0-beta.6 → 0.1.0-beta.7

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 (82) hide show
  1. package/dist/nextcloud-vue.cjs.js +13606 -1918
  2. package/dist/nextcloud-vue.cjs.js.map +1 -1
  3. package/dist/nextcloud-vue.css +1238 -270
  4. package/dist/nextcloud-vue.esm.js +13548 -1880
  5. package/dist/nextcloud-vue.esm.js.map +1 -1
  6. package/package.json +9 -4
  7. package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
  8. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +1 -11
  9. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +5 -1
  10. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +1 -1
  11. package/src/components/CnCard/CnCard.vue +415 -0
  12. package/src/components/CnCard/index.js +1 -0
  13. package/src/components/CnCardGrid/CnCardGrid.vue +20 -20
  14. package/src/components/CnChartWidget/CnChartWidget.vue +3 -1
  15. package/src/components/CnCopyDialog/CnCopyDialog.vue +7 -1
  16. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +4 -0
  17. package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -0
  18. package/src/components/CnDataTable/CnDataTable.vue +6 -2
  19. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +7 -1
  20. package/src/components/CnDetailCard/CnDetailCard.vue +12 -1
  21. package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
  22. package/src/components/CnDetailGrid/index.js +1 -0
  23. package/src/components/CnDetailPage/CnDetailPage.vue +157 -11
  24. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +3 -1
  25. package/src/components/CnFormDialog/CnFormDialog.vue +934 -920
  26. package/src/components/CnIcon/CnIcon.vue +1 -1
  27. package/src/components/CnIndexPage/CnIndexPage.vue +51 -9
  28. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +37 -9
  29. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
  30. package/src/components/CnInfoWidget/index.js +1 -0
  31. package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
  32. package/src/components/CnJsonViewer/index.js +1 -0
  33. package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
  34. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +7 -1
  35. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +7 -1
  36. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +1 -1
  37. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +1 -1
  38. package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
  39. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
  40. package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
  41. package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
  42. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +45 -668
  43. package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
  44. package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
  45. package/src/components/CnObjectSidebar/index.js +5 -0
  46. package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
  47. package/src/components/CnProgressBar/index.js +1 -0
  48. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +1 -1
  49. package/src/components/CnStatsBlock/CnStatsBlock.vue +27 -11
  50. package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
  51. package/src/components/CnStatsPanel/index.js +1 -0
  52. package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
  53. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +5 -1
  54. package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
  55. package/src/components/CnTableWidget/index.js +1 -0
  56. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +36 -1
  57. package/src/components/index.js +11 -0
  58. package/src/composables/useDashboardView.js +58 -12
  59. package/src/composables/useDetailView.js +3 -2
  60. package/src/composables/useListView.js +7 -6
  61. package/src/composables/useSubResource.js +3 -3
  62. package/src/css/badge.css +32 -0
  63. package/src/css/card.css +1 -0
  64. package/src/css/detail-page.css +74 -7
  65. package/src/index.js +16 -0
  66. package/src/mixins/gridLayout.js +118 -0
  67. package/src/store/createCrudStore.js +360 -0
  68. package/src/store/createSubResourcePlugin.js +5 -15
  69. package/src/store/index.js +1 -0
  70. package/src/store/plugins/auditTrails.js +346 -6
  71. package/src/store/plugins/lifecycle.js +4 -4
  72. package/src/store/plugins/registerMapping.js +18 -8
  73. package/src/store/plugins/relations.js +1 -1
  74. package/src/store/plugins/search.js +21 -8
  75. package/src/store/useObjectStore.js +30 -36
  76. package/src/utils/getTheme.js +9 -0
  77. package/src/utils/headers.js +13 -3
  78. package/src/utils/index.js +1 -0
  79. package/src/utils/schema.js +3 -3
  80. package/src/utils/widgetVisibility.js +162 -0
  81. package/src/components/CnObjectCard/eslint-setup.md +0 -235
  82. package/src/components/CnObjectCard/package.json-or.json +0 -132
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "0.1.0-beta.6",
3
+ "version": "0.1.0-beta.7",
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
  "license": "EUPL-1.2",
6
6
  "author": "Conduction B.V. <info@conduction.nl>",
7
- "main": "dist/nextcloud-vue.cjs",
7
+ "main": "dist/nextcloud-vue.cjs.js",
8
8
  "module": "dist/nextcloud-vue.esm.js",
9
9
  "style": "dist/nextcloud-vue.css",
10
10
  "types": "src/types/index.d.ts",
@@ -25,9 +25,12 @@
25
25
  "prepublishOnly": "npm run build"
26
26
  },
27
27
  "dependencies": {
28
+ "@codemirror/lang-html": "^6.4.11",
28
29
  "@codemirror/lang-json": "^6.0.2",
30
+ "@codemirror/lang-xml": "^6.1.0",
29
31
  "@nextcloud/capabilities": "^1.2.1",
30
32
  "@nextcloud/dialogs": "^7.3.0",
33
+ "@uiw/codemirror-theme-github": "^4.25.8",
31
34
  "gridstack": "^10.3.1",
32
35
  "vue-apexcharts": "^1.7.0",
33
36
  "vue-codemirror6": "^1.4.3"
@@ -43,11 +46,13 @@
43
46
  "vue-frag": "^1.4.3",
44
47
  "vue-material-design-icons": "^5.0.0"
45
48
  },
49
+ "overrides": {
50
+ "json5": "^2.2.3",
51
+ "loader-utils": "^1.4.2"
52
+ },
46
53
  "devDependencies": {
47
54
  "@babel/core": "^7.29.0",
48
- "@babel/plugin-transform-typescript": "^7.28.6",
49
55
  "@babel/preset-env": "^7.29.0",
50
- "@babel/preset-typescript": "^7.28.5",
51
56
  "@eslint/config-helpers": "^0.4.2",
52
57
  "@eslint/eslintrc": "^3.3.4",
53
58
  "@eslint/js": "^9.39.3",
@@ -31,7 +31,7 @@
31
31
  </div>
32
32
 
33
33
  <!-- Add button (primary) -->
34
- <NcButton type="primary" @click="$emit('add')">
34
+ <NcButton v-if="showAdd" type="primary" @click="$emit('add')">
35
35
  <template #icon>
36
36
  <CnIcon v-if="addIcon" :name="addIcon" :size="20" />
37
37
  <Plus v-else :size="20" />
@@ -218,6 +218,11 @@ export default {
218
218
  type: Boolean,
219
219
  default: false,
220
220
  },
221
+ /** Whether to show the Add button */
222
+ showAdd: {
223
+ type: Boolean,
224
+ default: true,
225
+ },
221
226
  },
222
227
 
223
228
  computed: {
@@ -32,8 +32,7 @@
32
32
  <!-- Register/schema selection step (optional slot) -->
33
33
  <slot
34
34
  v-if="$scopedSlots['register-schema-selection']"
35
- name="register-schema-selection"
36
- :proceed="proceedFromRegisterSchemaStep" />
35
+ name="register-schema-selection" />
37
36
 
38
37
  <!-- Main tabs -->
39
38
  <div v-else class="cn-advanced-form-dialog__tabs tabContainer">
@@ -46,7 +45,6 @@
46
45
  :update-field="updateField"
47
46
  :object-properties="objectPropertiesForSlot"
48
47
  :selected-property="selectedProperty"
49
- :handle-row-click="onRowClick"
50
48
  :get-property-display-name="getPropertyDisplayName"
51
49
  :get-property-validation-class="getPropertyValidationClass"
52
50
  :is-property-editable="isPropertyEditable"
@@ -321,10 +319,6 @@ export default {
321
319
  },
322
320
 
323
321
  methods: {
324
- proceedFromRegisterSchemaStep() {
325
- // Placeholder for slot consumers
326
- },
327
-
328
322
  initFormData(item) {
329
323
  if (item) {
330
324
  this.formData = JSON.parse(JSON.stringify(item))
@@ -358,10 +352,6 @@ export default {
358
352
  if (this.errors[key]) this.$delete(this.errors, key)
359
353
  },
360
354
 
361
- onRowClick(key, event) {
362
- // Forwarded for #tab-properties slot consumers — the sub-component handles it internally
363
- },
364
-
365
355
  /**
366
356
  * Proxy for slot consumers: exposes isPropertyEditable from the tab sub-component.
367
357
  * @param {string} key - Property key
@@ -130,7 +130,11 @@ export default {
130
130
  },
131
131
 
132
132
  methods: {
133
- /** The effective value for a key: formData override or the object's own value */
133
+ /**
134
+ * The effective value for a key: formData override or the object's own value
135
+ * @param {string} key - The property key to look up
136
+ * @param {*} objectValue - The fallback value from the object
137
+ */
134
138
  resolvedValue(key, objectValue) {
135
139
  return this.formData[key] !== undefined ? this.formData[key] : objectValue
136
140
  },
@@ -81,7 +81,7 @@ export default {
81
81
  /** Full JSON schema object */
82
82
  schema: { type: Object, default: null },
83
83
  /** Resolved current value (formData[key] ?? objectValue) */
84
- value: { default: null },
84
+ value: { type: [String, Number, Boolean, Object, Array], default: null },
85
85
  /** Whether this property is editable at all */
86
86
  isEditable: { type: Boolean, default: true },
87
87
  /** Whether this row is currently selected for editing */
@@ -0,0 +1,415 @@
1
+ <template>
2
+ <div
3
+ class="cn-card"
4
+ :class="rootClasses"
5
+ :style="activeStyles"
6
+ @click="onClick">
7
+ <div class="cn-card__header">
8
+ <h2 class="cn-card__title">
9
+ <slot name="icon">
10
+ <component :is="icon" v-if="icon" :size="iconSize" />
11
+ </slot>
12
+ <span ref="titleText" v-tooltip.bottom="computedTooltip" class="cn-card__title-text">{{ title }}</span>
13
+ </h2>
14
+ <div v-if="$slots.actions || $scopedSlots.actions" class="cn-card__actions">
15
+ <slot name="actions" />
16
+ </div>
17
+ <slot name="labels">
18
+ <span v-if="labels.length > 0" class="cn-card__labels">
19
+ <CnStatusBadge
20
+ v-for="(label, i) in labels"
21
+ :key="i"
22
+ :label="label.text"
23
+ :variant="label.variant || 'default'"
24
+ :solid="true" />
25
+ </span>
26
+ </slot>
27
+ </div>
28
+
29
+ <div class="cn-card__body">
30
+ <slot name="description">
31
+ <p v-if="description"
32
+ class="cn-card__description"
33
+ :style="descriptionStyle">
34
+ {{ description }}
35
+ </p>
36
+ </slot>
37
+
38
+ <div v-if="$slots.default || $scopedSlots.default" class="cn-card__content">
39
+ <slot />
40
+ </div>
41
+
42
+ <slot name="stats">
43
+ <div v-if="stats.length > 0" class="cn-card__stats">
44
+ <div v-for="(stat, i) in stats" :key="i" class="cn-card__stat">
45
+ <span class="cn-card__stat-label">{{ stat.label }}:</span>
46
+ <span class="cn-card__stat-value">{{ stat.value }}</span>
47
+ </div>
48
+ </div>
49
+ </slot>
50
+ </div>
51
+
52
+ <!-- Footer -->
53
+ <slot name="footer">
54
+ <div v-if="hasFooterContent" class="cn-card__footer">
55
+ <a
56
+ v-for="(link, i) in footerLinks"
57
+ :key="'link-' + i"
58
+ :href="link.url"
59
+ target="_blank"
60
+ rel="noopener noreferrer"
61
+ class="cn-card__footer-link">
62
+ <slot :name="'footer-link-icon-' + i" />
63
+ {{ link.label || link.url }}
64
+ </a>
65
+ <CnStatusBadge
66
+ v-for="(tag, i) in normalizedTags"
67
+ :key="'tag-' + i"
68
+ :label="tag.text"
69
+ :variant="tag.variant || 'default'"
70
+ size="small" />
71
+ </div>
72
+ </slot>
73
+ </div>
74
+ </template>
75
+
76
+ <script>
77
+ import { CnStatusBadge } from '../CnStatusBadge/index.js'
78
+
79
+ /**
80
+ * CnCard — Generic prop-driven card component.
81
+ *
82
+ * A flexible card for displaying entities with a title, icon, description,
83
+ * labels/badges, stats, and an optional active highlight state. Unlike
84
+ * CnObjectCard (schema-driven), CnCard takes direct props and is ideal
85
+ * for known, fixed-structure entities.
86
+ *
87
+ * @example Basic usage
88
+ * <CnCard
89
+ * title="My Source"
90
+ * description="A PostgreSQL data source"
91
+ * :icon="DatabaseArrowRightOutline"
92
+ * :stats="[{ label: 'Type', value: 'PostgreSQL' }]">
93
+ * <template #actions>
94
+ * <NcActions><NcActionButton @click="edit">Edit</NcActionButton></NcActions>
95
+ * </template>
96
+ * </CnCard>
97
+ *
98
+ * @example With labels and active state
99
+ * <CnCard
100
+ * title="My Organisation"
101
+ * :icon="OfficeBuilding"
102
+ * :active="isActive"
103
+ * active-variant="success"
104
+ * :labels="[
105
+ * { text: 'Default', variant: 'warning' },
106
+ * { text: 'Active', variant: 'success' },
107
+ * ]"
108
+ * :stats="[
109
+ * { label: 'Members', value: 12 },
110
+ * { label: 'Owner', value: 'Admin' },
111
+ * ]" />
112
+ */
113
+ export default {
114
+ name: 'CnCard',
115
+
116
+ components: {
117
+ CnStatusBadge,
118
+ },
119
+
120
+ props: {
121
+ /** Card title text */
122
+ title: {
123
+ type: String,
124
+ default: '',
125
+ },
126
+ /** Description text, displayed with line-clamp truncation */
127
+ description: {
128
+ type: String,
129
+ default: '',
130
+ },
131
+ /** Tooltip text for the title. If not set, falls back to description */
132
+ titleTooltip: {
133
+ type: String,
134
+ default: '',
135
+ },
136
+ /** Icon component (e.g., imported MDI icon). Rendered via <component :is> */
137
+ icon: {
138
+ type: [Object, Function],
139
+ default: null,
140
+ },
141
+ /** Icon size in pixels */
142
+ iconSize: {
143
+ type: Number,
144
+ default: 20,
145
+ },
146
+ /**
147
+ * Array of badge/label objects displayed inline with the title.
148
+ * Each entry: { text: string, variant?: string }
149
+ * Variant maps to CnStatusBadge variants: 'default'|'primary'|'success'|'warning'|'error'|'info'
150
+ * For labels with icons, use the #labels slot override and render CnStatusBadge
151
+ * manually with its #icon slot.
152
+ */
153
+ labels: {
154
+ type: Array,
155
+ default: () => [],
156
+ },
157
+ /**
158
+ * Array of stat rows displayed as label:value pairs.
159
+ * Each entry: { label: string, value: string|number }
160
+ */
161
+ stats: {
162
+ type: Array,
163
+ default: () => [],
164
+ },
165
+ /** Maximum lines for description truncation (CSS line-clamp) */
166
+ descriptionLines: {
167
+ type: Number,
168
+ default: 3,
169
+ },
170
+ /** Whether the card is in an active/highlighted state */
171
+ active: {
172
+ type: Boolean,
173
+ default: false,
174
+ },
175
+ /**
176
+ * Color variant for the active state border and background.
177
+ * Maps to Nextcloud CSS variables.
178
+ */
179
+ activeVariant: {
180
+ type: String,
181
+ default: 'success',
182
+ validator: (v) => ['success', 'primary', 'warning', 'error', 'info'].includes(v),
183
+ },
184
+ /** Whether the card is clickable (adds hover effect and cursor pointer) */
185
+ clickable: {
186
+ type: Boolean,
187
+ default: false,
188
+ },
189
+ /**
190
+ * Array of footer link objects. Each entry: { url: string, label?: string }
191
+ * Links are rendered as clickable anchors. Use the #footer-link-icon-{index} slot
192
+ * to add an icon before a specific link.
193
+ */
194
+ footerLinks: {
195
+ type: Array,
196
+ default: () => [],
197
+ },
198
+ /**
199
+ * Array of tag items for the footer. Accepts either strings or objects.
200
+ * String entries are converted to { text: string, variant: 'default' }.
201
+ * Object entries: { text: string, variant?: string }
202
+ */
203
+ tags: {
204
+ type: Array,
205
+ default: () => [],
206
+ },
207
+ },
208
+
209
+ data() {
210
+ return {
211
+ isTitleEllipsized: false,
212
+ }
213
+ },
214
+
215
+ computed: {
216
+ computedTooltip() {
217
+ if (this.titleTooltip) return this.titleTooltip
218
+ return this.isTitleEllipsized ? this.title : ''
219
+ },
220
+
221
+ rootClasses() {
222
+ return {
223
+ 'cn-card--active': this.active,
224
+ 'cn-card--clickable': this.clickable,
225
+ }
226
+ },
227
+
228
+ descriptionStyle() {
229
+ return {
230
+ '-webkit-line-clamp': this.descriptionLines,
231
+ 'line-clamp': this.descriptionLines,
232
+ }
233
+ },
234
+
235
+ normalizedTags() {
236
+ return this.tags.map(tag =>
237
+ typeof tag === 'string' ? { text: tag, variant: 'default' } : tag,
238
+ )
239
+ },
240
+
241
+ hasFooterContent() {
242
+ return this.footerLinks.length > 0 || this.tags.length > 0
243
+ },
244
+
245
+ activeStyles() {
246
+ if (!this.active) return {}
247
+ const variantMap = {
248
+ success: 'var(--color-success)',
249
+ primary: 'var(--color-primary-element)',
250
+ warning: 'var(--color-warning)',
251
+ error: 'var(--color-error)',
252
+ info: 'var(--color-info)',
253
+ }
254
+ return {
255
+ '--cn-card-active-border': variantMap[this.activeVariant] || variantMap.success,
256
+ }
257
+ },
258
+ },
259
+
260
+ mounted() {
261
+ this.checkTitleEllipsis()
262
+ this._resizeObserver = new ResizeObserver(() => this.checkTitleEllipsis())
263
+ this._resizeObserver.observe(this.$el)
264
+ },
265
+
266
+ beforeDestroy() {
267
+ if (this._resizeObserver) {
268
+ this._resizeObserver.disconnect()
269
+ }
270
+ },
271
+
272
+ methods: {
273
+ onClick(event) {
274
+ if (this.clickable) {
275
+ this.$emit('click', event)
276
+ }
277
+ },
278
+
279
+ checkTitleEllipsis() {
280
+ const el = this.$refs.titleText
281
+ this.isTitleEllipsized = el ? el.scrollWidth > el.clientWidth : false
282
+ },
283
+ },
284
+ }
285
+ </script>
286
+
287
+ <style scoped lang="scss">
288
+ .cn-card {
289
+ padding: 16px;
290
+ border: 1px solid var(--color-border);
291
+ border-radius: var(--border-radius-large);
292
+ background: var(--color-main-background);
293
+ height: 100%;
294
+ display: flex;
295
+ flex-direction: column;
296
+ }
297
+
298
+ .cn-card--active {
299
+ border: 2px solid var(--cn-card-active-border);
300
+ }
301
+
302
+ .cn-card--clickable {
303
+ cursor: pointer;
304
+ transition: box-shadow 0.2s ease, border-color 0.2s ease;
305
+
306
+ &:hover {
307
+ border-color: var(--color-primary-element);
308
+ box-shadow: 0 2px 8px var(--color-box-shadow);
309
+ }
310
+ }
311
+
312
+ .cn-card__header {
313
+ display: grid;
314
+ grid-template-columns: 1fr auto;
315
+ grid-template-rows: auto auto;
316
+ align-items: center;
317
+ border-bottom: 1px solid var(--color-border);
318
+ padding-block-end: 1rem;
319
+ margin-block-end: 0.5rem;
320
+ }
321
+
322
+ .cn-card__title {
323
+ display: flex;
324
+ align-items: center;
325
+ gap: 6px;
326
+ font-size: 16px;
327
+ margin: 0;
328
+ min-width: 0;
329
+ }
330
+
331
+ .cn-card__title-text {
332
+ overflow: hidden;
333
+ text-overflow: ellipsis;
334
+ white-space: nowrap;
335
+ }
336
+
337
+ .cn-card__labels {
338
+ grid-column: 1 / -1;
339
+ display: flex;
340
+ gap: 4px;
341
+ flex-wrap: wrap;
342
+ margin-top: 6px;
343
+ }
344
+
345
+ .cn-card__actions {
346
+ flex-shrink: 0;
347
+ margin-inline-start: 0.25rem;
348
+ }
349
+
350
+ .cn-card__body {
351
+ flex-grow: 1;
352
+ display: flex;
353
+ flex-direction: column;
354
+ justify-content: space-between;
355
+ }
356
+
357
+ .cn-card__description {
358
+ color: var(--color-text-lighter);
359
+ margin-bottom: 12px;
360
+ word-wrap: break-word;
361
+ overflow-wrap: break-word;
362
+ display: -webkit-box;
363
+ -webkit-box-orient: vertical;
364
+ overflow: hidden;
365
+ }
366
+
367
+ .cn-card__content {
368
+ margin-bottom: 12px;
369
+ }
370
+
371
+ .cn-card__stats {
372
+ display: flex;
373
+ flex-direction: column;
374
+ gap: 4px;
375
+ }
376
+
377
+ .cn-card__stat {
378
+ display: flex;
379
+ justify-content: space-between;
380
+ }
381
+
382
+ .cn-card__stat-label {
383
+ color: var(--color-text-lighter);
384
+ font-size: 12px;
385
+ }
386
+
387
+ .cn-card__stat-value {
388
+ font-weight: 600;
389
+ font-size: 12px;
390
+ }
391
+
392
+ .cn-card__footer {
393
+ display: flex;
394
+ flex-wrap: wrap;
395
+ gap: 8px;
396
+ align-items: center;
397
+ padding-top: 8px;
398
+ margin-top: 8px;
399
+ border-top: 1px solid var(--color-border);
400
+ }
401
+
402
+ .cn-card__footer-link {
403
+ display: inline-flex;
404
+ align-items: center;
405
+ gap: 4px;
406
+ font-size: 0.85em;
407
+ color: var(--color-primary-element);
408
+ text-decoration: none;
409
+ transition: color 0.2s;
410
+
411
+ &:hover {
412
+ text-decoration: underline;
413
+ }
414
+ }
415
+ </style>
@@ -0,0 +1 @@
1
+ export { default as CnCard } from './CnCard.vue'
@@ -18,28 +18,28 @@
18
18
 
19
19
  <!-- Card grid -->
20
20
  <div v-else class="cn-card-grid__grid">
21
- <slot
22
- v-for="object in objects"
23
- name="card"
24
- :object="object"
25
- :selected="isSelected(object)"
26
- :schema="schema">
27
- <CnObjectCard
28
- :key="object[rowKey]"
21
+ <div v-for="object in objects" :key="object[rowKey]">
22
+ <slot
23
+ name="card"
29
24
  :object="object"
30
- :schema="schema"
31
- :selectable="selectable"
32
25
  :selected="isSelected(object)"
33
- @click="$emit('click', object)"
34
- @select="toggleSelect(object)">
35
- <template v-if="$scopedSlots['card-actions']" #actions="{ object: obj }">
36
- <slot name="card-actions" :object="obj" />
37
- </template>
38
- <template v-if="$scopedSlots['card-badges']" #badges="{ object: obj }">
39
- <slot name="card-badges" :object="obj" />
40
- </template>
41
- </CnObjectCard>
42
- </slot>
26
+ :schema="schema">
27
+ <CnObjectCard
28
+ :object="object"
29
+ :schema="schema"
30
+ :selectable="selectable"
31
+ :selected="isSelected(object)"
32
+ @click="$emit('click', object)"
33
+ @select="toggleSelect(object)">
34
+ <template v-if="$scopedSlots['card-actions']" #actions="{ object: obj }">
35
+ <slot name="card-actions" :object="obj" />
36
+ </template>
37
+ <template v-if="$scopedSlots['card-badges']" #badges="{ object: obj }">
38
+ <slot name="card-badges" :object="obj" />
39
+ </template>
40
+ </CnObjectCard>
41
+ </slot>
42
+ </div>
43
43
  </div>
44
44
  </div>
45
45
  </template>
@@ -20,7 +20,9 @@
20
20
  :series="series" />
21
21
  <div v-else class="cn-chart-widget__fallback">
22
22
  <slot name="fallback">
23
- <p class="cn-chart-widget__error">{{ unavailableLabel }}</p>
23
+ <p class="cn-chart-widget__error">
24
+ {{ unavailableLabel }}
25
+ </p>
24
26
  </slot>
25
27
  </div>
26
28
  </div>
@@ -107,6 +107,11 @@ export default {
107
107
  type: String,
108
108
  default: 'title',
109
109
  },
110
+ /** Optional function to format the item name. Receives the item, returns a string. Overrides nameField when provided. */
111
+ nameFormatter: {
112
+ type: Function,
113
+ default: null,
114
+ },
110
115
  /** Dialog title */
111
116
  dialogTitle: {
112
117
  type: String,
@@ -138,6 +143,7 @@ export default {
138
143
 
139
144
  computed: {
140
145
  itemName() {
146
+ if (this.nameFormatter) return this.nameFormatter(this.item)
141
147
  return this.item[this.nameField] || this.item.name || this.item.title || this.item.id
142
148
  },
143
149
 
@@ -189,7 +195,7 @@ export default {
189
195
  * Set the result of the copy operation. Call this from the parent
190
196
  * after the API call completes.
191
197
  *
192
- * @param {{ success?: boolean, error?: string }} resultData
198
+ * @param {{ success?: boolean, error?: string }} resultData - Result data to pass to the dialog
193
199
  * @public
194
200
  */
195
201
  setResult(resultData) {
@@ -217,6 +217,10 @@ export default {
217
217
  overflow: hidden;
218
218
  }
219
219
 
220
+ :deep(.grid-stack-item-content:has(.cn-widget-wrapper--borderless)) {
221
+ background: transparent;
222
+ }
223
+
220
224
  :deep(.grid-stack-placeholder > .placeholder-content) {
221
225
  background: var(--color-primary-element-light);
222
226
  border: 2px dashed var(--color-primary-element);
@@ -72,6 +72,8 @@
72
72
  :icon-url="getWidgetIconUrl(item)"
73
73
  :icon-class="getWidgetIconClass(item)"
74
74
  :show-title="item.showTitle !== false"
75
+ :borderless="item.showTitle === false"
76
+ :flush="item.flush === true"
75
77
  :buttons="getWidgetButtons(item)"
76
78
  :style-config="item.styleConfig || {}">
77
79
  <slot :name="'widget-' + item.widgetId" :item="item" :widget="getWidgetDef(item.widgetId)" />
@@ -337,9 +337,13 @@ export default {
337
337
 
338
338
  toggleSelectAll() {
339
339
  if (this.allSelected) {
340
- this.$emit('select', [])
340
+ // Remove only current page IDs, preserving cross-page selections
341
+ const currentPageIds = new Set(this.rows.map((row) => row[this.rowKey]))
342
+ this.$emit('select', this.selectedIds.filter((id) => !currentPageIds.has(id)))
341
343
  } else {
342
- this.$emit('select', this.rows.map((row) => row[this.rowKey]))
344
+ // Add current page IDs to existing selections
345
+ const merged = new Set([...this.selectedIds, ...this.rows.map((row) => row[this.rowKey])])
346
+ this.$emit('select', [...merged])
343
347
  }
344
348
  /** @event select-all Emitted when select-all checkbox is toggled. */
345
349
  this.$emit('select-all', !this.allSelected)