@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.
- package/dist/nextcloud-vue.cjs.js +13606 -1918
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +1238 -270
- package/dist/nextcloud-vue.esm.js +13548 -1880
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +9 -4
- package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +1 -11
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +5 -1
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +1 -1
- package/src/components/CnCard/CnCard.vue +415 -0
- package/src/components/CnCard/index.js +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +20 -20
- package/src/components/CnChartWidget/CnChartWidget.vue +3 -1
- package/src/components/CnCopyDialog/CnCopyDialog.vue +7 -1
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +4 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -0
- package/src/components/CnDataTable/CnDataTable.vue +6 -2
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +7 -1
- package/src/components/CnDetailCard/CnDetailCard.vue +12 -1
- package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
- package/src/components/CnDetailGrid/index.js +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +157 -11
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +3 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +934 -920
- package/src/components/CnIcon/CnIcon.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +51 -9
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +37 -9
- package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
- package/src/components/CnInfoWidget/index.js +1 -0
- package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
- package/src/components/CnJsonViewer/index.js +1 -0
- package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +7 -1
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +7 -1
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +1 -1
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +1 -1
- package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
- package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
- package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
- package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +45 -668
- package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
- package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
- package/src/components/CnObjectSidebar/index.js +5 -0
- package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
- package/src/components/CnProgressBar/index.js +1 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +1 -1
- package/src/components/CnStatsBlock/CnStatsBlock.vue +27 -11
- package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
- package/src/components/CnStatsPanel/index.js +1 -0
- package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +5 -1
- package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
- package/src/components/CnTableWidget/index.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +36 -1
- package/src/components/index.js +11 -0
- package/src/composables/useDashboardView.js +58 -12
- package/src/composables/useDetailView.js +3 -2
- package/src/composables/useListView.js +7 -6
- package/src/composables/useSubResource.js +3 -3
- package/src/css/badge.css +32 -0
- package/src/css/card.css +1 -0
- package/src/css/detail-page.css +74 -7
- package/src/index.js +16 -0
- package/src/mixins/gridLayout.js +118 -0
- package/src/store/createCrudStore.js +360 -0
- package/src/store/createSubResourcePlugin.js +5 -15
- package/src/store/index.js +1 -0
- package/src/store/plugins/auditTrails.js +346 -6
- package/src/store/plugins/lifecycle.js +4 -4
- package/src/store/plugins/registerMapping.js +18 -8
- package/src/store/plugins/relations.js +1 -1
- package/src/store/plugins/search.js +21 -8
- package/src/store/useObjectStore.js +30 -36
- package/src/utils/getTheme.js +9 -0
- package/src/utils/headers.js +13 -3
- package/src/utils/index.js +1 -0
- package/src/utils/schema.js +3 -3
- package/src/utils/widgetVisibility.js +162 -0
- package/src/components/CnObjectCard/eslint-setup.md +0 -235
- 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.
|
|
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
|
-
/**
|
|
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
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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">
|
|
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
|
-
|
|
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
|
-
|
|
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)
|