@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.
- package/dist/nextcloud-vue.cjs.js +7282 -3443
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +719 -100
- package/dist/nextcloud-vue.esm.js +7120 -3300
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +3 -2
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +36 -3
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +34 -19
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +312 -36
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +983 -64
- package/src/components/CnAdvancedFormDialog/index.js +3 -0
- package/src/components/CnAppLoading/CnAppLoading.vue +93 -0
- package/src/components/CnAppLoading/index.js +3 -0
- package/src/components/CnAppNav/CnAppNav.vue +269 -0
- package/src/components/CnAppNav/index.js +3 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +201 -0
- package/src/components/CnAppRoot/index.js +3 -0
- package/src/components/CnColorPicker/CnColorPicker.vue +251 -0
- package/src/components/CnColorPicker/index.js +1 -0
- package/src/components/CnContextMenu/CnContextMenu.vue +41 -4
- package/src/components/CnDashboardPage/CnDashboardPage.vue +8 -0
- package/src/components/CnDependencyMissing/CnDependencyMissing.vue +152 -0
- package/src/components/CnDependencyMissing/index.js +3 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +27 -16
- package/src/components/CnIndexPage/CnIndexPage.vue +36 -6
- package/src/components/CnPageRenderer/CnPageRenderer.vue +278 -0
- package/src/components/CnPageRenderer/index.js +4 -0
- package/src/components/CnPageRenderer/pageTypes.js +37 -0
- package/src/components/CnRowActions/CnRowActions.vue +44 -3
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +4 -0
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +103 -74
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +30 -2
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +16 -12
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +9 -4
- package/src/components/index.js +7 -1
- package/src/composables/index.js +2 -0
- package/src/composables/useAppManifest.js +115 -0
- package/src/composables/useAppStatus.js +107 -0
- package/src/css/CnSchemaFormDialog.css +22 -0
- package/src/index.js +24 -2
- package/src/schemas/app-manifest.schema.json +153 -0
- package/src/types/index.d.ts +9 -0
- package/src/types/manifest.d.ts +88 -0
- package/src/utils/index.js +1 -1
- package/src/utils/schema.js +157 -2
- package/src/utils/validateManifest.js +113 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.16",
|
|
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>",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"@uiw/codemirror-theme-github": "^4.25.8",
|
|
48
48
|
"gridstack": "^10.3.1",
|
|
49
49
|
"vue-apexcharts": "^1.7.0",
|
|
50
|
-
"vue-codemirror6": "^1.4.3"
|
|
50
|
+
"vue-codemirror6": "^1.4.3",
|
|
51
|
+
"vue-color": "^2.8.2"
|
|
51
52
|
},
|
|
52
53
|
"peerDependencies": {
|
|
53
54
|
"@nextcloud/axios": "^2.0.0",
|
|
@@ -37,8 +37,13 @@
|
|
|
37
37
|
<!-- Main tabs -->
|
|
38
38
|
<div v-else class="cn-advanced-form-dialog__tabs tabContainer">
|
|
39
39
|
<BTabs v-model="activeTab" content-class="mt-3" justified>
|
|
40
|
-
<!-- Properties tab
|
|
41
|
-
|
|
40
|
+
<!-- Properties tab — disabled when the active schema has no
|
|
41
|
+
properties to render (a bare JSON blob is still
|
|
42
|
+
editable via the Data tab). -->
|
|
43
|
+
<BTab
|
|
44
|
+
v-if="showPropertiesTable"
|
|
45
|
+
title="Properties"
|
|
46
|
+
:disabled="!hasSchemaProperties">
|
|
42
47
|
<slot
|
|
43
48
|
name="tab-properties"
|
|
44
49
|
:form-data="formData"
|
|
@@ -130,7 +135,7 @@ import CnMetadataTab from './CnMetadataTab.vue'
|
|
|
130
135
|
import CnDataTab from './CnDataTab.vue'
|
|
131
136
|
|
|
132
137
|
/** Schema types for which we have built-in inline editing support in the properties table. */
|
|
133
|
-
const EDITABLE_SUPPORTED_TYPES = ['string', 'number', 'integer', 'boolean']
|
|
138
|
+
const EDITABLE_SUPPORTED_TYPES = ['string', 'number', 'integer', 'boolean', 'array', 'object']
|
|
134
139
|
|
|
135
140
|
/**
|
|
136
141
|
* CnAdvancedFormDialog — Create/edit dialog with properties table (click-to-edit), JSON tab, and optional store integration.
|
|
@@ -229,6 +234,24 @@ export default {
|
|
|
229
234
|
return !!this.item
|
|
230
235
|
},
|
|
231
236
|
|
|
237
|
+
/**
|
|
238
|
+
* True when the active schema declares at least one (non-metadata)
|
|
239
|
+
* property the Properties tab can render. Used to disable the tab
|
|
240
|
+
* when there's nothing for it to show — the Data tab still works.
|
|
241
|
+
*/
|
|
242
|
+
hasSchemaProperties() {
|
|
243
|
+
const props = this.schema?.properties || {}
|
|
244
|
+
const exclude = this.excludeFields || []
|
|
245
|
+
const include = this.includeFields
|
|
246
|
+
for (const key of Object.keys(props)) {
|
|
247
|
+
if (key === '@self' || key === 'id') continue
|
|
248
|
+
if (exclude.includes(key)) continue
|
|
249
|
+
if (include && !include.includes(key)) continue
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
return false
|
|
253
|
+
},
|
|
254
|
+
|
|
232
255
|
resolvedFields() {
|
|
233
256
|
return fieldsFromSchema(this.schema, {
|
|
234
257
|
exclude: this.excludeFields,
|
|
@@ -297,6 +320,16 @@ export default {
|
|
|
297
320
|
this.initFormData(newItem)
|
|
298
321
|
},
|
|
299
322
|
},
|
|
323
|
+
hasSchemaProperties: {
|
|
324
|
+
immediate: true,
|
|
325
|
+
handler(hasProps) {
|
|
326
|
+
// When the Properties tab is disabled, skip past it so we
|
|
327
|
+
// don't land on a non-interactive tab on first render.
|
|
328
|
+
if (!hasProps && this.activeTab === 0 && this.showPropertiesTable) {
|
|
329
|
+
this.activeTab = this.resolvedShowMetadataTab ? 1 : (this.showJsonTab ? 1 : 0)
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
},
|
|
300
333
|
jsonData(newVal) {
|
|
301
334
|
if (!this.isInternalUpdate && this.isValidJson(newVal)) {
|
|
302
335
|
this.updateFormFromJson()
|
|
@@ -12,28 +12,15 @@
|
|
|
12
12
|
</tr>
|
|
13
13
|
</thead>
|
|
14
14
|
<tbody>
|
|
15
|
-
<tr
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<td class="cn-advanced-form-dialog__table-col-expanded">
|
|
20
|
-
{{ metadataId }}
|
|
21
|
-
</td>
|
|
22
|
-
</tr>
|
|
23
|
-
<tr class="cn-advanced-form-dialog__table-row">
|
|
15
|
+
<tr
|
|
16
|
+
v-for="[label, value] in resolvedRows"
|
|
17
|
+
:key="label"
|
|
18
|
+
class="cn-advanced-form-dialog__table-row">
|
|
24
19
|
<td class="cn-advanced-form-dialog__table-col-constrained">
|
|
25
|
-
{{
|
|
20
|
+
{{ label }}
|
|
26
21
|
</td>
|
|
27
22
|
<td class="cn-advanced-form-dialog__table-col-expanded">
|
|
28
|
-
{{
|
|
29
|
-
</td>
|
|
30
|
-
</tr>
|
|
31
|
-
<tr class="cn-advanced-form-dialog__table-row">
|
|
32
|
-
<td class="cn-advanced-form-dialog__table-col-constrained">
|
|
33
|
-
{{ t('nextcloud-vue', 'Updated') }}
|
|
34
|
-
</td>
|
|
35
|
-
<td class="cn-advanced-form-dialog__table-col-expanded">
|
|
36
|
-
{{ metadataUpdated }}
|
|
23
|
+
{{ value }}
|
|
37
24
|
</td>
|
|
38
25
|
</tr>
|
|
39
26
|
</tbody>
|
|
@@ -42,12 +29,24 @@
|
|
|
42
29
|
</template>
|
|
43
30
|
|
|
44
31
|
<script>
|
|
32
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
33
|
+
|
|
45
34
|
export default {
|
|
46
35
|
name: 'CnMetadataTab',
|
|
47
36
|
|
|
48
37
|
props: {
|
|
49
38
|
item: { type: Object, default: null },
|
|
50
39
|
formData: { type: Object, default: () => ({}) },
|
|
40
|
+
/**
|
|
41
|
+
* Additional `[label, value]` rows appended to (or, when `replaceRows` is true,
|
|
42
|
+
* replacing) the default ID/Created/Updated rows. Use this to surface
|
|
43
|
+
* domain-specific metadata (version, owner, custom timestamps, etc.).
|
|
44
|
+
*/
|
|
45
|
+
extraRows: { type: Array, default: () => [] },
|
|
46
|
+
/**
|
|
47
|
+
* When true, render only `extraRows` and skip the default ID/Created/Updated rows.
|
|
48
|
+
*/
|
|
49
|
+
replaceRows: { type: Boolean, default: false },
|
|
51
50
|
},
|
|
52
51
|
|
|
53
52
|
computed: {
|
|
@@ -67,7 +66,23 @@ export default {
|
|
|
67
66
|
const v = o?.updated
|
|
68
67
|
return v ? new Date(v).toLocaleString() : '—'
|
|
69
68
|
},
|
|
69
|
+
|
|
70
|
+
defaultRows() {
|
|
71
|
+
return [
|
|
72
|
+
[this.t('nextcloud-vue', 'ID'), this.metadataId],
|
|
73
|
+
[this.t('nextcloud-vue', 'Created'), this.metadataCreated],
|
|
74
|
+
[this.t('nextcloud-vue', 'Updated'), this.metadataUpdated],
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
resolvedRows() {
|
|
79
|
+
const extra = Array.isArray(this.extraRows) ? this.extraRows : []
|
|
80
|
+
if (this.replaceRows) return extra
|
|
81
|
+
return [...this.defaultRows, ...extra]
|
|
82
|
+
},
|
|
70
83
|
},
|
|
84
|
+
|
|
85
|
+
methods: { t },
|
|
71
86
|
}
|
|
72
87
|
</script>
|
|
73
88
|
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
<th class="cn-advanced-form-dialog__table-col-expanded">
|
|
10
10
|
{{ t('nextcloud-vue', 'Value') }}
|
|
11
11
|
</th>
|
|
12
|
+
<th
|
|
13
|
+
v-if="hasRowActionsSlot"
|
|
14
|
+
class="cn-advanced-form-dialog__table-col-actions">
|
|
15
|
+
<slot name="row-actions-header" />
|
|
16
|
+
</th>
|
|
12
17
|
</tr>
|
|
13
18
|
</thead>
|
|
14
19
|
<tbody>
|
|
@@ -18,18 +23,24 @@
|
|
|
18
23
|
class="cn-advanced-form-dialog__table-row"
|
|
19
24
|
:class="{
|
|
20
25
|
'cn-advanced-form-dialog__table-row--selected': selectedProperty === key,
|
|
21
|
-
'cn-advanced-form-dialog__table-row--edited':
|
|
26
|
+
'cn-advanced-form-dialog__table-row--edited': isValueChanged(key),
|
|
22
27
|
'cn-advanced-form-dialog__table-row--non-editable': !isPropertyEditable(key, resolvedValue(key, value)),
|
|
23
28
|
[getPropertyValidationClass(key, value)]: validationDisplay === 'indicator',
|
|
24
29
|
}"
|
|
25
30
|
@click="handleRowClick(key, $event)">
|
|
26
|
-
<td class="cn-advanced-form-dialog__table-col-constrained cn-advanced-form-dialog__prop-cell"
|
|
31
|
+
<td class="cn-advanced-form-dialog__table-col-constrained cn-advanced-form-dialog__prop-cell"
|
|
32
|
+
:style="getPropCellStyle(key, value)">
|
|
27
33
|
<div class="cn-advanced-form-dialog__prop-cell-content">
|
|
28
34
|
<AlertCircle
|
|
29
35
|
v-if="validationDisplay === 'indicator' && getPropertyValidationState(key, resolvedValue(key, value)) === 'invalid'"
|
|
30
36
|
class="cn-advanced-form-dialog__validation-icon cn-advanced-form-dialog__validation-icon--error"
|
|
31
37
|
:size="16"
|
|
32
38
|
:title="getPropertyErrorMessage(key, resolvedValue(key, value))" />
|
|
39
|
+
<Alert
|
|
40
|
+
v-else-if="validationDisplay === 'indicator' && isValueChanged(key) && getPropertyValidationState(key, resolvedValue(key, value)) !== 'invalid'"
|
|
41
|
+
class="cn-advanced-form-dialog__validation-icon cn-advanced-form-dialog__validation-icon--edited"
|
|
42
|
+
:size="16"
|
|
43
|
+
:title="t('nextcloud-vue', 'This field has been changed')" />
|
|
33
44
|
<Alert
|
|
34
45
|
v-else-if="validationDisplay === 'indicator' && getPropertyValidationState(key, resolvedValue(key, value)) === 'warning'"
|
|
35
46
|
class="cn-advanced-form-dialog__validation-icon cn-advanced-form-dialog__validation-icon--warning"
|
|
@@ -46,19 +57,58 @@
|
|
|
46
57
|
:size="16"
|
|
47
58
|
:title="getEditabilityWarning(key, resolvedValue(key, value)) || ''" />
|
|
48
59
|
<span :title="getPropertyTooltip(key)">{{ getPropertyDisplayName(key) }}</span>
|
|
60
|
+
<span
|
|
61
|
+
v-if="isRequired(key)"
|
|
62
|
+
class="cn-advanced-form-dialog__required-indicator"
|
|
63
|
+
:title="t('nextcloud-vue', 'Required')"
|
|
64
|
+
aria-label="required">*</span>
|
|
65
|
+
<span
|
|
66
|
+
v-if="isImmutableHint(key)"
|
|
67
|
+
class="cn-advanced-form-dialog__immutable-badge"
|
|
68
|
+
:title="t('nextcloud-vue', 'This value can be set on creation but cannot be changed afterwards.')">
|
|
69
|
+
{{ t('nextcloud-vue', 'Set once') }}
|
|
70
|
+
</span>
|
|
49
71
|
</div>
|
|
50
72
|
</td>
|
|
51
73
|
<td class="cn-advanced-form-dialog__table-col-expanded cn-advanced-form-dialog__value-cell">
|
|
52
|
-
<
|
|
53
|
-
|
|
74
|
+
<slot
|
|
75
|
+
name="value-cell"
|
|
54
76
|
:property-key="key"
|
|
55
|
-
:
|
|
56
|
-
:value="resolvedValue(key, value)"
|
|
57
|
-
:is-editable="isPropertyEditable(key, resolvedValue(key, value))"
|
|
77
|
+
:value="value"
|
|
78
|
+
:resolved-value="resolvedValue(key, value)"
|
|
58
79
|
:is-editing="selectedProperty === key"
|
|
80
|
+
:is-editable="isPropertyEditable(key, resolvedValue(key, value))"
|
|
59
81
|
:display-name="getPropertyDisplayName(key)"
|
|
82
|
+
:schema-prop="schema && schema.properties && schema.properties[key]"
|
|
60
83
|
:editability-warning="getPropertyEditabilityWarning(key, resolvedValue(key, value))"
|
|
61
|
-
|
|
84
|
+
:on-update="(v) => onPropertyValueUpdate(key, v)">
|
|
85
|
+
<CnPropertyValueCell
|
|
86
|
+
:ref="'cell-' + key"
|
|
87
|
+
:property-key="key"
|
|
88
|
+
:schema="schema"
|
|
89
|
+
:value="resolvedValue(key, value)"
|
|
90
|
+
:is-editable="isPropertyEditable(key, resolvedValue(key, value))"
|
|
91
|
+
:is-editing="selectedProperty === key"
|
|
92
|
+
:display-name="getPropertyDisplayName(key)"
|
|
93
|
+
:editability-warning="getPropertyEditabilityWarning(key, resolvedValue(key, value))"
|
|
94
|
+
:widget="(propertyOverrides[key] && propertyOverrides[key].widget) || null"
|
|
95
|
+
:select-options="(propertyOverrides[key] && propertyOverrides[key].selectOptions) || null"
|
|
96
|
+
:select-multiple="propertyOverrides[key] ? propertyOverrides[key].selectMultiple !== false : true"
|
|
97
|
+
:textarea-rows="(propertyOverrides[key] && propertyOverrides[key].textareaRows) || 4"
|
|
98
|
+
@update:value="onPropertyValueUpdate(key, $event)" />
|
|
99
|
+
</slot>
|
|
100
|
+
</td>
|
|
101
|
+
<td
|
|
102
|
+
v-if="hasRowActionsSlot"
|
|
103
|
+
class="cn-advanced-form-dialog__table-col-actions"
|
|
104
|
+
@click.stop>
|
|
105
|
+
<slot
|
|
106
|
+
name="row-actions"
|
|
107
|
+
:property-key="key"
|
|
108
|
+
:value="value"
|
|
109
|
+
:resolved-value="resolvedValue(key, value)"
|
|
110
|
+
:is-editable="isPropertyEditable(key, resolvedValue(key, value))"
|
|
111
|
+
:is-schema-property="!!(schema && schema.properties && Object.prototype.hasOwnProperty.call(schema.properties, key))" />
|
|
62
112
|
</td>
|
|
63
113
|
</tr>
|
|
64
114
|
</tbody>
|
|
@@ -67,6 +117,7 @@
|
|
|
67
117
|
</template>
|
|
68
118
|
|
|
69
119
|
<script>
|
|
120
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
70
121
|
import AlertCircle from 'vue-material-design-icons/AlertCircle.vue'
|
|
71
122
|
import Alert from 'vue-material-design-icons/Alert.vue'
|
|
72
123
|
import LockOutline from 'vue-material-design-icons/LockOutline.vue'
|
|
@@ -89,13 +140,40 @@ export default {
|
|
|
89
140
|
item: { type: Object, default: null },
|
|
90
141
|
formData: { type: Object, default: () => ({}) },
|
|
91
142
|
selectedProperty: { type: String, default: null },
|
|
92
|
-
editableTypes: { type: Array, default: () => ['string', 'number', 'integer', 'boolean'] },
|
|
143
|
+
editableTypes: { type: Array, default: () => ['string', 'number', 'integer', 'boolean', 'array', 'object'] },
|
|
93
144
|
validationDisplay: { type: String, default: 'indicator' },
|
|
94
145
|
excludeFields: { type: Array, default: () => [] },
|
|
95
146
|
includeFields: { type: Array, default: null },
|
|
147
|
+
/**
|
|
148
|
+
* When false (default), properties whose schema entry has `const`
|
|
149
|
+
* set are filtered out of the list — the user can't change them so
|
|
150
|
+
* they only add noise. Set to `true` to render them anyway (e.g. for
|
|
151
|
+
* debugging or admin views). `immutable` / `readOnly` properties are
|
|
152
|
+
* always rendered regardless of this flag; they're just non-editable
|
|
153
|
+
* once they have a value.
|
|
154
|
+
*/
|
|
155
|
+
showConstantProperties: { type: Boolean, default: false },
|
|
156
|
+
/**
|
|
157
|
+
* Per-property overrides forwarded to CnPropertyValueCell. Keyed by property key.
|
|
158
|
+
* Each entry may contain: `{ widget, selectOptions, selectMultiple, textareaRows }`.
|
|
159
|
+
*/
|
|
160
|
+
propertyOverrides: { type: Object, default: () => ({}) },
|
|
161
|
+
/**
|
|
162
|
+
* Override the left-edge indicator color on the property-name cell.
|
|
163
|
+
* When null (default) the CSS default is used (var(--color-primary)).
|
|
164
|
+
* Pass "none" to remove the indicator and let the per-row validation
|
|
165
|
+
* colors (green/yellow/red) on the <tr> show through instead.
|
|
166
|
+
* Any other CSS color string is applied directly.
|
|
167
|
+
*/
|
|
168
|
+
propCellColor: { type: String, default: null },
|
|
96
169
|
},
|
|
97
170
|
|
|
98
171
|
computed: {
|
|
172
|
+
propCellStyle() {
|
|
173
|
+
if (this.propCellColor === null) return undefined
|
|
174
|
+
if (this.propCellColor === 'none') return { boxShadow: 'none' }
|
|
175
|
+
return { boxShadow: `inset 3px 0 0 0 ${this.propCellColor}` }
|
|
176
|
+
},
|
|
99
177
|
objectProperties() {
|
|
100
178
|
const schemaProps = this.schema?.properties || {}
|
|
101
179
|
const obj = this.item || {}
|
|
@@ -105,6 +183,7 @@ export default {
|
|
|
105
183
|
if (k === '@self' || k === 'id') return false
|
|
106
184
|
if (exclude.includes(k)) return false
|
|
107
185
|
if (include && !include.includes(k)) return false
|
|
186
|
+
if (schemaProps[k]?.hideOnForm === true) return false
|
|
108
187
|
return true
|
|
109
188
|
}
|
|
110
189
|
const existing = Object.entries(obj).filter(([k]) => filterKey(k))
|
|
@@ -112,24 +191,94 @@ export default {
|
|
|
112
191
|
for (const [key, prop] of Object.entries(schemaProps)) {
|
|
113
192
|
if (!filterKey(key)) continue
|
|
114
193
|
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
115
|
-
|
|
116
|
-
switch (prop.type) {
|
|
117
|
-
case 'string': def = prop.const ?? ''; break
|
|
118
|
-
case 'number':
|
|
119
|
-
case 'integer': def = 0; break
|
|
120
|
-
case 'boolean': def = false; break
|
|
121
|
-
case 'array': def = []; break
|
|
122
|
-
case 'object': def = {}; break
|
|
123
|
-
default: def = ''
|
|
124
|
-
}
|
|
125
|
-
missing.push([key, def])
|
|
194
|
+
missing.push([key, this.defaultForProperty(prop)])
|
|
126
195
|
}
|
|
127
196
|
}
|
|
128
|
-
|
|
197
|
+
const all = [...existing, ...missing]
|
|
198
|
+
const filtered = this.showConstantProperties
|
|
199
|
+
? all
|
|
200
|
+
: all.filter(([key]) => !this.isConstantOrImmutableKey(key))
|
|
201
|
+
// Sort: schema `order` ascending (0 first), unspecified last; preserve
|
|
202
|
+
// schema/property declaration order as a stable tiebreaker.
|
|
203
|
+
const indexFor = (key) => {
|
|
204
|
+
const i = Object.keys(schemaProps).indexOf(key)
|
|
205
|
+
return i === -1 ? Number.MAX_SAFE_INTEGER : i
|
|
206
|
+
}
|
|
207
|
+
const orderFor = (key) => {
|
|
208
|
+
const o = schemaProps[key]?.order
|
|
209
|
+
return typeof o === 'number' ? o : Number.MAX_SAFE_INTEGER
|
|
210
|
+
}
|
|
211
|
+
return filtered
|
|
212
|
+
.map(([k, v], i) => ({ k, v, order: orderFor(k), idx: indexFor(k), insertion: i }))
|
|
213
|
+
.sort((a, b) => (a.order - b.order) || (a.idx - b.idx) || (a.insertion - b.insertion))
|
|
214
|
+
.map(({ k, v }) => [k, v])
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* True when at least one property in the (unfiltered) list is constant or immutable.
|
|
219
|
+
* Useful for parents that want to render a show/hide-toggle button only when relevant.
|
|
220
|
+
*/
|
|
221
|
+
hasConstantOrImmutableProperties() {
|
|
222
|
+
const schemaProps = this.schema?.properties || {}
|
|
223
|
+
const obj = this.item || {}
|
|
224
|
+
const exclude = this.excludeFields || []
|
|
225
|
+
const include = this.includeFields
|
|
226
|
+
const keys = new Set([
|
|
227
|
+
...Object.keys(obj),
|
|
228
|
+
...Object.keys(schemaProps),
|
|
229
|
+
])
|
|
230
|
+
for (const k of keys) {
|
|
231
|
+
if (k === '@self' || k === 'id') continue
|
|
232
|
+
if (exclude.includes(k)) continue
|
|
233
|
+
if (include && !include.includes(k)) continue
|
|
234
|
+
if (schemaProps[k]?.hideOnForm === true) continue
|
|
235
|
+
if (this.isConstantOrImmutableKey(k)) return true
|
|
236
|
+
}
|
|
237
|
+
return false
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
hasRowActionsSlot() {
|
|
241
|
+
return !!this.$scopedSlots['row-actions']
|
|
129
242
|
},
|
|
130
243
|
},
|
|
131
244
|
|
|
132
245
|
methods: {
|
|
246
|
+
t,
|
|
247
|
+
|
|
248
|
+
getPropCellStyle(key, value) {
|
|
249
|
+
if (this.validationDisplay !== 'indicator') {
|
|
250
|
+
return this.propCellStyle
|
|
251
|
+
}
|
|
252
|
+
const resolvedVal = this.resolvedValue(key, value)
|
|
253
|
+
const state = this.getPropertyValidationState(key, resolvedVal)
|
|
254
|
+
if (state === 'invalid') {
|
|
255
|
+
return { boxShadow: 'inset 3px 0 0 0 var(--color-error)' }
|
|
256
|
+
}
|
|
257
|
+
if (this.isValueChanged(key)) {
|
|
258
|
+
return { boxShadow: 'inset 3px 0 0 0 var(--color-warning)' }
|
|
259
|
+
}
|
|
260
|
+
if (state === 'valid') {
|
|
261
|
+
return { boxShadow: 'inset 3px 0 0 0 var(--color-success)' }
|
|
262
|
+
}
|
|
263
|
+
if (state === 'warning') {
|
|
264
|
+
return { boxShadow: 'inset 3px 0 0 0 var(--color-warning)' }
|
|
265
|
+
}
|
|
266
|
+
if (state === 'new') {
|
|
267
|
+
return { boxShadow: 'inset 3px 0 0 0 var(--color-primary-element)' }
|
|
268
|
+
}
|
|
269
|
+
return this.propCellStyle
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
isValueChanged(key) {
|
|
273
|
+
if (this.formData[key] === undefined) return false
|
|
274
|
+
const original = this.item ? this.item[key] : undefined
|
|
275
|
+
const current = this.formData[key]
|
|
276
|
+
if (typeof current === 'object' || typeof original === 'object') {
|
|
277
|
+
return JSON.stringify(current) !== JSON.stringify(original)
|
|
278
|
+
}
|
|
279
|
+
return current !== original
|
|
280
|
+
},
|
|
281
|
+
|
|
133
282
|
/**
|
|
134
283
|
* The effective value for a key: formData override or the object's own value
|
|
135
284
|
* @param {string} key - The property key to look up
|
|
@@ -139,15 +288,92 @@ export default {
|
|
|
139
288
|
return this.formData[key] !== undefined ? this.formData[key] : objectValue
|
|
140
289
|
},
|
|
141
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Initial display value for a schema property that doesn't yet exist on the
|
|
293
|
+
* object. Honors `default` and `const` first, then falls back to the
|
|
294
|
+
* type-appropriate empty value.
|
|
295
|
+
* @param {object} prop - The schema property entry.
|
|
296
|
+
*/
|
|
297
|
+
defaultForProperty(prop) {
|
|
298
|
+
if (!prop) return ''
|
|
299
|
+
if (prop.default !== undefined) return prop.default
|
|
300
|
+
if (prop.const !== undefined) return prop.const
|
|
301
|
+
switch (prop.type) {
|
|
302
|
+
case 'string': return ''
|
|
303
|
+
case 'number':
|
|
304
|
+
case 'integer': return 0
|
|
305
|
+
case 'boolean': return false
|
|
306
|
+
case 'array': return []
|
|
307
|
+
case 'object': return {}
|
|
308
|
+
default: return ''
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
|
|
142
312
|
onPropertyValueUpdate(key, value) {
|
|
143
313
|
this.$emit('update:property-value', { key, value })
|
|
144
314
|
},
|
|
145
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Whether a property is marked required either via `schema.required: [...]`
|
|
318
|
+
* (the JSON-Schema-canonical place) or via `prop.required: true` on the
|
|
319
|
+
* property entry itself (a non-standard but commonly seen variant).
|
|
320
|
+
* @param {string} key - Property key.
|
|
321
|
+
* @return {boolean}
|
|
322
|
+
*/
|
|
323
|
+
isRequired(key) {
|
|
324
|
+
if ((this.schema?.required || []).includes(key)) return true
|
|
325
|
+
const prop = this.schema?.properties?.[key]
|
|
326
|
+
return !!(prop && prop.required === true)
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Whether the property's value is fixed by the schema (`const`) and
|
|
331
|
+
* should be hide-able via the show/hide toggle. Note: `immutable` /
|
|
332
|
+
* `readOnly` are NOT considered constant — they're set on creation
|
|
333
|
+
* and locked afterward, but should remain visible in the form.
|
|
334
|
+
* @param {string} key - Property key.
|
|
335
|
+
* @return {boolean}
|
|
336
|
+
*/
|
|
337
|
+
/**
|
|
338
|
+
* True when an "immutable" / "readOnly" hint badge should be shown
|
|
339
|
+
* for this property — i.e. the prop is settable on creation but
|
|
340
|
+
* locks once persisted, AND it isn't already locked. Once locked the
|
|
341
|
+
* lock icon takes over and the badge would be redundant.
|
|
342
|
+
* @param {string} key - Property key.
|
|
343
|
+
* @return {boolean}
|
|
344
|
+
*/
|
|
345
|
+
isImmutableHint(key) {
|
|
346
|
+
const prop = this.schema?.properties?.[key]
|
|
347
|
+
if (!prop) return false
|
|
348
|
+
if (prop.const !== undefined) return false
|
|
349
|
+
const lockOnce = prop.immutable === true || prop.readOnly === true
|
|
350
|
+
if (!lockOnce) return false
|
|
351
|
+
return this.isPropertyEditable(key, null)
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
isConstantOrImmutableKey(key) {
|
|
355
|
+
const prop = this.schema?.properties?.[key]
|
|
356
|
+
if (!prop) return false
|
|
357
|
+
return prop.const !== undefined
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
// `value` is intentionally unused — kept in the signature for callers
|
|
361
|
+
// that already pass it (slot consumers, the cell, the row click
|
|
362
|
+
// handler). Editability is now driven by the persisted `item`.
|
|
363
|
+
// eslint-disable-next-line no-unused-vars
|
|
146
364
|
isPropertyEditable(key, value) {
|
|
147
365
|
const prop = this.schema?.properties?.[key]
|
|
148
366
|
if (!prop) return true
|
|
149
367
|
if (prop.const !== undefined) return false
|
|
150
|
-
|
|
368
|
+
// `immutable` / `readOnly` mean "settable on creation, locked
|
|
369
|
+
// once persisted". Use the persisted `item` (not the live value
|
|
370
|
+
// the user is typing) as the source of truth — otherwise the
|
|
371
|
+
// field would lock the moment the first character is entered.
|
|
372
|
+
const lockOnce = prop.immutable === true || prop.readOnly === true
|
|
373
|
+
if (lockOnce) {
|
|
374
|
+
const persisted = this.item && this.item[key]
|
|
375
|
+
if (persisted != null && persisted !== '') return false
|
|
376
|
+
}
|
|
151
377
|
const type = prop.type || 'string'
|
|
152
378
|
return this.editableTypes.includes(type)
|
|
153
379
|
},
|
|
@@ -197,6 +423,8 @@ export default {
|
|
|
197
423
|
case 'string':
|
|
198
424
|
if (typeof value !== 'string') return false
|
|
199
425
|
if (schemaProperty?.format === 'date-time' && !this.isValidDate(value)) return false
|
|
426
|
+
if (schemaProperty?.format === 'email' && !this.isValidEmail(value)) return false
|
|
427
|
+
if (schemaProperty?.format === 'uri' && !this.isValidUri(value)) return false
|
|
200
428
|
if (schemaProperty?.const && value !== schemaProperty.const) return false
|
|
201
429
|
return true
|
|
202
430
|
case 'number':
|
|
@@ -229,6 +457,12 @@ export default {
|
|
|
229
457
|
if (prop.format === 'date-time' && !this.isValidDate(value)) {
|
|
230
458
|
return `Property '${key}' should be a valid date-time value.`
|
|
231
459
|
}
|
|
460
|
+
if (prop.format === 'email' && !this.isValidEmail(value)) {
|
|
461
|
+
return `Property '${key}' should be a valid email address.`
|
|
462
|
+
}
|
|
463
|
+
if (prop.format === 'uri' && !this.isValidUri(value)) {
|
|
464
|
+
return `Property '${key}' should be a valid URI.`
|
|
465
|
+
}
|
|
232
466
|
if (prop.const && value !== prop.const) {
|
|
233
467
|
return `Property '${key}' should be '${prop.const}' but is '${value}'.`
|
|
234
468
|
}
|
|
@@ -285,6 +519,20 @@ export default {
|
|
|
285
519
|
const d = new Date(v)
|
|
286
520
|
return d instanceof Date && !Number.isNaN(d.getTime())
|
|
287
521
|
},
|
|
522
|
+
|
|
523
|
+
isValidEmail(v) {
|
|
524
|
+
if (!v) return false
|
|
525
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
isValidUri(v) {
|
|
529
|
+
if (!v) return false
|
|
530
|
+
try {
|
|
531
|
+
return !!new URL(v)
|
|
532
|
+
} catch {
|
|
533
|
+
return false
|
|
534
|
+
}
|
|
535
|
+
},
|
|
288
536
|
},
|
|
289
537
|
}
|
|
290
538
|
</script>
|
|
@@ -293,7 +541,6 @@ export default {
|
|
|
293
541
|
.cn-advanced-form-dialog__table-container {
|
|
294
542
|
background: var(--color-main-background);
|
|
295
543
|
border-radius: var(--border-radius);
|
|
296
|
-
overflow: hidden;
|
|
297
544
|
box-shadow: 0 2px 4px var(--color-box-shadow);
|
|
298
545
|
border: 1px solid var(--color-border);
|
|
299
546
|
margin-bottom: calc(5 * var(--default-grid-baseline));
|
|
@@ -313,6 +560,13 @@ export default {
|
|
|
313
560
|
vertical-align: middle;
|
|
314
561
|
}
|
|
315
562
|
|
|
563
|
+
/* Selected (editing) row: align the constrained label cell to the top of the
|
|
564
|
+
value cell so tall inputs (textarea, JSON editor) do not visually overflow
|
|
565
|
+
into the next row. */
|
|
566
|
+
.cn-advanced-form-dialog__table-row--selected td {
|
|
567
|
+
vertical-align: top;
|
|
568
|
+
}
|
|
569
|
+
|
|
316
570
|
.cn-advanced-form-dialog__table th {
|
|
317
571
|
background: var(--color-background-dark);
|
|
318
572
|
font-weight: 500;
|
|
@@ -337,8 +591,7 @@ export default {
|
|
|
337
591
|
}
|
|
338
592
|
|
|
339
593
|
.cn-advanced-form-dialog__table-row--edited {
|
|
340
|
-
background-color: var(--color-
|
|
341
|
-
box-shadow: inset 3px 0 0 0 var(--color-success);
|
|
594
|
+
background-color: var(--color-warning-light);
|
|
342
595
|
}
|
|
343
596
|
|
|
344
597
|
.cn-advanced-form-dialog__table-row--non-editable {
|
|
@@ -351,22 +604,12 @@ export default {
|
|
|
351
604
|
cursor: not-allowed !important;
|
|
352
605
|
}
|
|
353
606
|
|
|
354
|
-
.cn-advanced-form-dialog__table-row--valid {
|
|
355
|
-
box-shadow: inset 3px 0 0 0 var(--color-success);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
607
|
.cn-advanced-form-dialog__table-row--invalid {
|
|
359
608
|
background-color: var(--color-error-light);
|
|
360
|
-
box-shadow: inset 3px 0 0 0 var(--color-error);
|
|
361
609
|
}
|
|
362
610
|
|
|
363
611
|
.cn-advanced-form-dialog__table-row--warning {
|
|
364
612
|
background-color: var(--color-warning-light);
|
|
365
|
-
box-shadow: inset 3px 0 0 0 var(--color-warning);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
.cn-advanced-form-dialog__table-row--new {
|
|
369
|
-
box-shadow: inset 3px 0 0 0 var(--color-primary-element);
|
|
370
613
|
}
|
|
371
614
|
|
|
372
615
|
.cn-advanced-form-dialog__table-col-constrained {
|
|
@@ -382,10 +625,15 @@ export default {
|
|
|
382
625
|
min-width: 200px;
|
|
383
626
|
}
|
|
384
627
|
|
|
628
|
+
.cn-advanced-form-dialog__table-col-actions {
|
|
629
|
+
width: 56px;
|
|
630
|
+
text-align: right;
|
|
631
|
+
white-space: nowrap;
|
|
632
|
+
}
|
|
633
|
+
|
|
385
634
|
.cn-advanced-form-dialog__prop-cell {
|
|
386
635
|
width: 30%;
|
|
387
636
|
font-weight: 600;
|
|
388
|
-
box-shadow: inset 3px 0 0 0 var(--color-primary);
|
|
389
637
|
}
|
|
390
638
|
|
|
391
639
|
.cn-advanced-form-dialog__value-cell {
|
|
@@ -394,6 +642,10 @@ export default {
|
|
|
394
642
|
border-radius: 4px;
|
|
395
643
|
}
|
|
396
644
|
|
|
645
|
+
.cn-advanced-form-dialog__value-cell > * {
|
|
646
|
+
max-width: 100%;
|
|
647
|
+
}
|
|
648
|
+
|
|
397
649
|
.cn-advanced-form-dialog__prop-cell-content {
|
|
398
650
|
display: flex;
|
|
399
651
|
align-items: center;
|
|
@@ -408,6 +660,10 @@ export default {
|
|
|
408
660
|
color: var(--color-error);
|
|
409
661
|
}
|
|
410
662
|
|
|
663
|
+
.cn-advanced-form-dialog__validation-icon--edited {
|
|
664
|
+
color: var(--color-warning);
|
|
665
|
+
}
|
|
666
|
+
|
|
411
667
|
.cn-advanced-form-dialog__validation-icon--warning {
|
|
412
668
|
color: var(--color-warning);
|
|
413
669
|
}
|
|
@@ -419,4 +675,24 @@ export default {
|
|
|
419
675
|
.cn-advanced-form-dialog__validation-icon--new {
|
|
420
676
|
color: var(--color-primary-element);
|
|
421
677
|
}
|
|
678
|
+
|
|
679
|
+
.cn-advanced-form-dialog__required-indicator {
|
|
680
|
+
color: var(--color-error-text);
|
|
681
|
+
font-weight: bold;
|
|
682
|
+
cursor: help;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.cn-advanced-form-dialog__immutable-badge {
|
|
686
|
+
display: inline-block;
|
|
687
|
+
padding: 1px 6px;
|
|
688
|
+
border-radius: 10px;
|
|
689
|
+
background: var(--color-background-dark);
|
|
690
|
+
color: var(--color-text-maxcontrast);
|
|
691
|
+
font-size: 0.75em;
|
|
692
|
+
font-weight: 500;
|
|
693
|
+
text-transform: uppercase;
|
|
694
|
+
letter-spacing: 0.02em;
|
|
695
|
+
cursor: help;
|
|
696
|
+
white-space: nowrap;
|
|
697
|
+
}
|
|
422
698
|
</style>
|