@conduction/nextcloud-vue 0.1.0-beta.1 → 0.1.0-beta.10
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/README.md +226 -0
- package/css/index.css +5 -0
- package/dist/nextcloud-vue.cjs +67614 -0
- package/dist/nextcloud-vue.cjs.js +76311 -5905
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.cjs.map +1 -0
- package/dist/nextcloud-vue.css +3279 -203
- package/dist/nextcloud-vue.esm.js +76240 -5882
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +89 -63
- package/src/components/CnActionsBar/CnActionsBar.vue +254 -0
- package/src/components/CnActionsBar/index.js +1 -0
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +569 -0
- package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +422 -0
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
- package/src/components/CnAdvancedFormDialog/index.js +1 -0
- package/src/components/CnCard/CnCard.vue +415 -0
- package/src/components/CnCard/index.js +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +23 -20
- package/src/components/CnCardGrid/index.js +1 -1
- package/src/components/CnCellRenderer/index.js +1 -1
- package/src/components/CnChartWidget/CnChartWidget.vue +318 -0
- package/src/components/CnChartWidget/index.js +1 -0
- package/src/components/CnConfigurationCard/index.js +1 -1
- package/src/components/CnContextMenu/CnContextMenu.vue +142 -0
- package/src/components/CnContextMenu/index.js +1 -0
- package/src/components/CnCopyDialog/CnCopyDialog.vue +257 -0
- package/src/components/CnCopyDialog/index.js +1 -0
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +229 -0
- package/src/components/CnDashboardGrid/index.js +1 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +396 -0
- package/src/components/CnDashboardPage/index.js +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +24 -16
- package/src/components/CnDataTable/index.js +1 -1
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +177 -0
- package/src/components/CnDeleteDialog/index.js +1 -0
- package/src/components/CnDetailCard/CnDetailCard.vue +225 -0
- package/src/components/CnDetailCard/index.js +1 -0
- package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
- package/src/components/CnDetailGrid/index.js +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +431 -0
- package/src/components/CnDetailPage/index.js +1 -0
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +12 -2
- package/src/components/CnFacetSidebar/index.js +1 -1
- package/src/components/CnFilterBar/index.js +1 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +934 -0
- package/src/components/CnFormDialog/index.js +1 -0
- package/src/components/CnIcon/CnIcon.vue +89 -0
- package/src/components/CnIcon/index.js +1 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +589 -291
- package/src/components/CnIndexPage/index.js +1 -1
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +535 -0
- package/src/components/CnIndexSidebar/index.js +1 -0
- package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
- package/src/components/CnInfoWidget/index.js +1 -0
- package/src/components/CnItemCard/CnItemCard.vue +134 -0
- package/src/components/CnItemCard/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/CnKpiGrid/index.js +1 -1
- package/src/components/CnMassActionBar/CnMassActionBar.vue +6 -5
- package/src/components/CnMassActionBar/index.js +1 -1
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +16 -9
- package/src/components/CnMassCopyDialog/index.js +1 -1
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +16 -9
- package/src/components/CnMassDeleteDialog/index.js +1 -1
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +8 -7
- package/src/components/CnMassExportDialog/index.js +1 -1
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +20 -17
- package/src/components/CnMassImportDialog/index.js +1 -1
- package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
- package/src/components/CnNoteCard/index.js +1 -0
- package/src/components/CnNotesCard/CnNotesCard.vue +415 -0
- package/src/components/CnNotesCard/index.js +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +3 -1
- package/src/components/CnObjectCard/index.js +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +853 -0
- package/src/components/CnObjectDataWidget/index.js +1 -0
- package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +288 -0
- package/src/components/CnObjectMetadataWidget/index.js +1 -0
- 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 +254 -0
- package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
- package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
- package/src/components/CnObjectSidebar/index.js +6 -0
- package/src/components/CnPageHeader/CnPageHeader.vue +61 -0
- package/src/components/CnPageHeader/index.js +1 -0
- package/src/components/CnPagination/CnPagination.vue +7 -6
- package/src/components/CnPagination/index.js +1 -1
- package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
- package/src/components/CnProgressBar/index.js +1 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -0
- package/src/components/CnRegisterMapping/index.js +1 -0
- package/src/components/CnRowActions/CnRowActions.vue +25 -3
- package/src/components/CnRowActions/index.js +1 -1
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
- package/src/components/CnSchemaFormDialog/index.js +1 -0
- package/src/components/CnSettingsCard/index.js +1 -1
- package/src/components/CnSettingsSection/index.js +1 -1
- package/src/components/CnStatsBlock/CnStatsBlock.vue +89 -19
- package/src/components/CnStatsBlock/index.js +1 -1
- 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/CnStatusBadge/index.js +1 -1
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +544 -0
- package/src/components/CnTabbedFormDialog/index.js +1 -0
- package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
- package/src/components/CnTableWidget/index.js +1 -0
- package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
- package/src/components/CnTasksCard/index.js +1 -0
- package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
- package/src/components/CnTileWidget/index.js +1 -0
- package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
- package/src/components/CnTimelineStages/index.js +1 -0
- package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
- package/src/components/CnUserActionMenu/index.js +1 -0
- package/src/components/CnVersionInfoCard/index.js +1 -1
- package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
- package/src/components/CnWidgetRenderer/index.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +246 -0
- package/src/components/CnWidgetWrapper/index.js +1 -0
- package/src/components/index.js +57 -25
- package/src/composables/index.js +5 -3
- package/src/composables/useContextMenu.js +126 -0
- package/src/composables/useDashboardView.js +286 -0
- package/src/composables/useDetailView.js +290 -132
- package/src/composables/useListView.js +364 -153
- package/src/composables/useSubResource.js +142 -142
- package/src/constants/metadata.js +30 -0
- package/src/css/CnSchemaFormDialog.css +546 -0
- package/src/css/__sample_nextcloud_tokens.css +110 -0
- package/src/css/actions-bar.css +54 -0
- package/src/css/badge.css +83 -51
- package/src/css/card.css +129 -128
- package/src/css/context-menu.css +20 -0
- package/src/css/dashboard.css +70 -0
- package/src/css/detail-page.css +235 -0
- package/src/css/detail.css +68 -68
- package/src/css/index-page.css +44 -0
- package/src/css/index-sidebar.css +193 -0
- package/src/css/index.css +17 -8
- package/src/css/layout.css +90 -90
- package/src/css/page-header.css +35 -0
- package/src/css/pagination.css +72 -72
- package/src/css/table.css +142 -143
- package/src/css/timeline-stages.css +220 -0
- package/src/css/utilities.css +46 -46
- package/src/index.js +91 -50
- package/src/mixins/gridLayout.js +118 -0
- package/src/store/createCrudStore.js +360 -0
- package/src/store/createSubResourcePlugin.js +125 -135
- package/src/store/index.js +4 -3
- package/src/store/plugins/auditTrails.js +357 -17
- package/src/store/plugins/files.js +250 -186
- package/src/store/plugins/index.js +7 -4
- package/src/store/plugins/lifecycle.js +180 -180
- package/src/store/plugins/registerMapping.js +195 -0
- package/src/store/plugins/relations.js +68 -68
- package/src/store/plugins/search.js +385 -0
- package/src/store/plugins/selection.js +104 -0
- package/src/store/useObjectStore.js +823 -625
- package/src/types/auditTrail.d.ts +32 -32
- package/src/types/file.d.ts +23 -23
- package/src/types/index.d.ts +35 -35
- package/src/types/notification.d.ts +36 -36
- package/src/types/object.d.ts +40 -40
- package/src/types/organisation.d.ts +41 -41
- package/src/types/register.d.ts +25 -25
- package/src/types/schema.d.ts +39 -39
- package/src/types/shared.d.ts +79 -79
- package/src/types/source.d.ts +14 -14
- package/src/types/task.d.ts +31 -31
- package/src/utils/errors.js +96 -96
- package/src/utils/getTheme.js +9 -0
- package/src/utils/headers.js +80 -44
- package/src/utils/id.js +13 -0
- package/src/utils/index.js +4 -3
- package/src/utils/schema.js +422 -287
- package/src/utils/widgetVisibility.js +162 -0
- package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
- package/src/components/CnDetailViewLayout/index.js +0 -1
- package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
- package/src/components/CnEmptyState/index.js +0 -1
- package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
- package/src/components/CnListViewLayout/index.js +0 -1
- package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
- package/src/components/CnViewModeToggle/index.js +0 -1
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
CnObjectDataWidget — Schema-driven editable data grid widget.
|
|
3
|
+
|
|
4
|
+
Displays object properties in a CSS grid layout, auto-generated from a JSON Schema.
|
|
5
|
+
Each cell shows a formatted value. Clicking an editable cell opens an inline editor
|
|
6
|
+
matching the property type (text, select, date, checkbox, textarea, etc.).
|
|
7
|
+
When any value is changed, a Save button appears in the widget header.
|
|
8
|
+
|
|
9
|
+
Supports per-property overrides for order, grid span, visibility, editability, and widget type.
|
|
10
|
+
-->
|
|
11
|
+
<template>
|
|
12
|
+
<CnDetailCard :title="title" :icon="iconComponent">
|
|
13
|
+
<template #actions>
|
|
14
|
+
<NcButton
|
|
15
|
+
v-if="isDirty"
|
|
16
|
+
type="primary"
|
|
17
|
+
:disabled="saving"
|
|
18
|
+
@click="save">
|
|
19
|
+
<template #icon>
|
|
20
|
+
<NcLoadingIcon v-if="saving" :size="20" />
|
|
21
|
+
<ContentSaveOutline v-else :size="20" />
|
|
22
|
+
</template>
|
|
23
|
+
{{ saveLabel }}
|
|
24
|
+
</NcButton>
|
|
25
|
+
<NcButton
|
|
26
|
+
v-if="isDirty"
|
|
27
|
+
@click="discard">
|
|
28
|
+
{{ discardLabel }}
|
|
29
|
+
</NcButton>
|
|
30
|
+
<slot name="actions" />
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<!-- Empty state -->
|
|
34
|
+
<div v-if="!resolvedFields.length" class="cn-object-data-widget__empty">
|
|
35
|
+
{{ emptyLabel }}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Grid -->
|
|
39
|
+
<div
|
|
40
|
+
v-else
|
|
41
|
+
class="cn-object-data-widget__grid"
|
|
42
|
+
:style="gridStyle">
|
|
43
|
+
<div
|
|
44
|
+
v-for="field in resolvedFields"
|
|
45
|
+
:key="field.key"
|
|
46
|
+
class="cn-object-data-widget__cell"
|
|
47
|
+
:style="cellStyle(field)">
|
|
48
|
+
<!-- Label -->
|
|
49
|
+
<div class="cn-object-data-widget__label">
|
|
50
|
+
{{ field.label }}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Editing mode for this field -->
|
|
54
|
+
<div
|
|
55
|
+
v-if="editingField === field.key"
|
|
56
|
+
class="cn-object-data-widget__editor">
|
|
57
|
+
<!-- Per-field slot override -->
|
|
58
|
+
<slot
|
|
59
|
+
v-if="$scopedSlots['field-' + field.key]"
|
|
60
|
+
:name="'field-' + field.key"
|
|
61
|
+
:field="field"
|
|
62
|
+
:value="editData[field.key]"
|
|
63
|
+
:update="(val) => updateField(field.key, val)"
|
|
64
|
+
:cancel="cancelEdit" />
|
|
65
|
+
|
|
66
|
+
<!-- Auto-generated editor -->
|
|
67
|
+
<template v-else>
|
|
68
|
+
<!-- Text / Email / URL -->
|
|
69
|
+
<NcTextField
|
|
70
|
+
v-if="field.widget === 'text' || field.widget === 'email' || field.widget === 'url'"
|
|
71
|
+
ref="activeEditor"
|
|
72
|
+
:value="editData[field.key] != null ? String(editData[field.key]) : ''"
|
|
73
|
+
:type="field.widget === 'email' ? 'email' : field.widget === 'url' ? 'url' : 'text'"
|
|
74
|
+
:placeholder="field.description"
|
|
75
|
+
@update:value="val => updateField(field.key, val)"
|
|
76
|
+
@keydown.native.enter="commitEdit"
|
|
77
|
+
@keydown.native.escape="cancelEdit" />
|
|
78
|
+
|
|
79
|
+
<!-- Number -->
|
|
80
|
+
<NcTextField
|
|
81
|
+
v-else-if="field.widget === 'number'"
|
|
82
|
+
ref="activeEditor"
|
|
83
|
+
:value="editData[field.key] != null ? String(editData[field.key]) : ''"
|
|
84
|
+
type="number"
|
|
85
|
+
:placeholder="field.description"
|
|
86
|
+
@update:value="val => updateField(field.key, val !== '' ? Number(val) : null)"
|
|
87
|
+
@keydown.native.enter="commitEdit"
|
|
88
|
+
@keydown.native.escape="cancelEdit" />
|
|
89
|
+
|
|
90
|
+
<!-- Textarea -->
|
|
91
|
+
<textarea
|
|
92
|
+
v-else-if="field.widget === 'textarea'"
|
|
93
|
+
ref="activeEditor"
|
|
94
|
+
class="cn-object-data-widget__textarea"
|
|
95
|
+
:value="editData[field.key] || ''"
|
|
96
|
+
:placeholder="field.description"
|
|
97
|
+
rows="4"
|
|
98
|
+
@input="updateField(field.key, $event.target.value)"
|
|
99
|
+
@keydown.escape="cancelEdit" />
|
|
100
|
+
|
|
101
|
+
<!-- Select -->
|
|
102
|
+
<NcSelect
|
|
103
|
+
v-else-if="field.widget === 'select'"
|
|
104
|
+
ref="activeEditor"
|
|
105
|
+
:options="getSelectOptions(field)"
|
|
106
|
+
:value="getSelectedOption(field)"
|
|
107
|
+
:clearable="!field.required"
|
|
108
|
+
@input="onSelectChange(field, $event)"
|
|
109
|
+
@close="commitEdit" />
|
|
110
|
+
|
|
111
|
+
<!-- Multiselect -->
|
|
112
|
+
<NcSelect
|
|
113
|
+
v-else-if="field.widget === 'multiselect'"
|
|
114
|
+
ref="activeEditor"
|
|
115
|
+
:options="getMultiselectOptions(field)"
|
|
116
|
+
:value="getSelectedMultiselectOptions(field)"
|
|
117
|
+
:multiple="true"
|
|
118
|
+
:clearable="true"
|
|
119
|
+
@input="onMultiselectChange(field, $event)" />
|
|
120
|
+
|
|
121
|
+
<!-- Tags -->
|
|
122
|
+
<NcSelect
|
|
123
|
+
v-else-if="field.widget === 'tags'"
|
|
124
|
+
ref="activeEditor"
|
|
125
|
+
:value="editData[field.key] || []"
|
|
126
|
+
:multiple="true"
|
|
127
|
+
:taggable="true"
|
|
128
|
+
:clearable="true"
|
|
129
|
+
@input="val => updateField(field.key, val)" />
|
|
130
|
+
|
|
131
|
+
<!-- Checkbox / Switch -->
|
|
132
|
+
<NcCheckboxRadioSwitch
|
|
133
|
+
v-else-if="field.widget === 'checkbox'"
|
|
134
|
+
:checked="!!editData[field.key]"
|
|
135
|
+
type="switch"
|
|
136
|
+
@update:checked="val => { updateField(field.key, val); commitEdit() }">
|
|
137
|
+
{{ editData[field.key] ? 'Yes' : 'No' }}
|
|
138
|
+
</NcCheckboxRadioSwitch>
|
|
139
|
+
|
|
140
|
+
<!-- Date -->
|
|
141
|
+
<NcTextField
|
|
142
|
+
v-else-if="field.widget === 'date'"
|
|
143
|
+
ref="activeEditor"
|
|
144
|
+
:value="editData[field.key] || ''"
|
|
145
|
+
type="date"
|
|
146
|
+
@update:value="val => updateField(field.key, val)"
|
|
147
|
+
@keydown.native.enter="commitEdit"
|
|
148
|
+
@keydown.native.escape="cancelEdit" />
|
|
149
|
+
|
|
150
|
+
<!-- Datetime -->
|
|
151
|
+
<NcTextField
|
|
152
|
+
v-else-if="field.widget === 'datetime'"
|
|
153
|
+
ref="activeEditor"
|
|
154
|
+
:value="editData[field.key] || ''"
|
|
155
|
+
type="datetime-local"
|
|
156
|
+
@update:value="val => updateField(field.key, val)"
|
|
157
|
+
@keydown.native.enter="commitEdit"
|
|
158
|
+
@keydown.native.escape="cancelEdit" />
|
|
159
|
+
|
|
160
|
+
<!-- Fallback: text -->
|
|
161
|
+
<NcTextField
|
|
162
|
+
v-else
|
|
163
|
+
ref="activeEditor"
|
|
164
|
+
:value="editData[field.key] != null ? String(editData[field.key]) : ''"
|
|
165
|
+
:placeholder="field.description"
|
|
166
|
+
@update:value="val => updateField(field.key, val)"
|
|
167
|
+
@keydown.native.enter="commitEdit"
|
|
168
|
+
@keydown.native.escape="cancelEdit" />
|
|
169
|
+
</template>
|
|
170
|
+
|
|
171
|
+
<!-- Confirm/Cancel for non-auto-committing editors -->
|
|
172
|
+
<div
|
|
173
|
+
v-if="field.widget !== 'checkbox'"
|
|
174
|
+
class="cn-object-data-widget__editor-actions">
|
|
175
|
+
<NcButton type="tertiary-no-background" @click="commitEdit">
|
|
176
|
+
<template #icon>
|
|
177
|
+
<Check :size="20" />
|
|
178
|
+
</template>
|
|
179
|
+
</NcButton>
|
|
180
|
+
<NcButton type="tertiary-no-background" @click="cancelEdit">
|
|
181
|
+
<template #icon>
|
|
182
|
+
<Close :size="20" />
|
|
183
|
+
</template>
|
|
184
|
+
</NcButton>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<!-- Display mode -->
|
|
189
|
+
<div
|
|
190
|
+
v-else
|
|
191
|
+
class="cn-object-data-widget__value"
|
|
192
|
+
:class="{
|
|
193
|
+
'cn-object-data-widget__value--editable': isEditable(field),
|
|
194
|
+
'cn-object-data-widget__value--empty': isValueEmpty(field.key),
|
|
195
|
+
}"
|
|
196
|
+
:tabindex="isEditable(field) ? 0 : -1"
|
|
197
|
+
:role="isEditable(field) ? 'button' : undefined"
|
|
198
|
+
:aria-label="isEditable(field) ? 'Click to edit ' + field.label : undefined"
|
|
199
|
+
@click="isEditable(field) && startEdit(field)"
|
|
200
|
+
@keydown.enter="isEditable(field) && startEdit(field)">
|
|
201
|
+
<!-- Per-field display slot override -->
|
|
202
|
+
<slot
|
|
203
|
+
v-if="$scopedSlots['display-' + field.key]"
|
|
204
|
+
:name="'display-' + field.key"
|
|
205
|
+
:field="field"
|
|
206
|
+
:value="displayValues[field.key]"
|
|
207
|
+
:raw="objectData[field.key]" />
|
|
208
|
+
<template v-else>
|
|
209
|
+
{{ displayValues[field.key] }}
|
|
210
|
+
</template>
|
|
211
|
+
<Pencil
|
|
212
|
+
v-if="isEditable(field)"
|
|
213
|
+
class="cn-object-data-widget__edit-icon"
|
|
214
|
+
:size="14" />
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</CnDetailCard>
|
|
219
|
+
</template>
|
|
220
|
+
|
|
221
|
+
<script>
|
|
222
|
+
import { NcButton, NcLoadingIcon, NcTextField, NcSelect, NcCheckboxRadioSwitch } from '@nextcloud/vue'
|
|
223
|
+
import { CnDetailCard } from '../CnDetailCard/index.js'
|
|
224
|
+
import ContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue'
|
|
225
|
+
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
|
226
|
+
import Check from 'vue-material-design-icons/Check.vue'
|
|
227
|
+
import Close from 'vue-material-design-icons/Close.vue'
|
|
228
|
+
import { fieldsFromSchema, formatValue } from '../../utils/schema.js'
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* CnObjectDataWidget — Schema-driven editable data grid widget.
|
|
232
|
+
*
|
|
233
|
+
* Renders object properties in a configurable CSS grid. Each property is displayed
|
|
234
|
+
* as a label-value cell. Editable cells can be clicked to switch to inline editing.
|
|
235
|
+
* Uses the objectStore to persist changes.
|
|
236
|
+
*
|
|
237
|
+
* @example Basic usage
|
|
238
|
+
* <CnObjectDataWidget
|
|
239
|
+
* title="Character Info"
|
|
240
|
+
* :schema="schema"
|
|
241
|
+
* :object-data="character"
|
|
242
|
+
* object-type="characters"
|
|
243
|
+
* :overrides="{
|
|
244
|
+
* name: { order: 1, gridColumn: 2 },
|
|
245
|
+
* description: { order: 2, gridColumn: 3, gridRow: 2 },
|
|
246
|
+
* status: { order: 3 },
|
|
247
|
+
* internalId: { hidden: true },
|
|
248
|
+
* }" />
|
|
249
|
+
*
|
|
250
|
+
* @example Read-only mode
|
|
251
|
+
* <CnObjectDataWidget
|
|
252
|
+
* title="Summary"
|
|
253
|
+
* :schema="schema"
|
|
254
|
+
* :object-data="item"
|
|
255
|
+
* :editable="false" />
|
|
256
|
+
*/
|
|
257
|
+
export default {
|
|
258
|
+
name: 'CnObjectDataWidget',
|
|
259
|
+
|
|
260
|
+
components: {
|
|
261
|
+
NcButton,
|
|
262
|
+
NcLoadingIcon,
|
|
263
|
+
NcTextField,
|
|
264
|
+
NcSelect,
|
|
265
|
+
NcCheckboxRadioSwitch,
|
|
266
|
+
CnDetailCard,
|
|
267
|
+
ContentSaveOutline,
|
|
268
|
+
Pencil,
|
|
269
|
+
Check,
|
|
270
|
+
Close,
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
props: {
|
|
274
|
+
/** Widget title shown in the card header */
|
|
275
|
+
title: {
|
|
276
|
+
type: String,
|
|
277
|
+
default: 'Data',
|
|
278
|
+
},
|
|
279
|
+
/** Optional MDI icon component for the header */
|
|
280
|
+
icon: {
|
|
281
|
+
type: [Object, Function],
|
|
282
|
+
default: null,
|
|
283
|
+
},
|
|
284
|
+
/**
|
|
285
|
+
* The JSON Schema describing the object's properties.
|
|
286
|
+
* Must have a `properties` field.
|
|
287
|
+
*/
|
|
288
|
+
schema: {
|
|
289
|
+
type: Object,
|
|
290
|
+
required: true,
|
|
291
|
+
},
|
|
292
|
+
/**
|
|
293
|
+
* The object data to display and edit.
|
|
294
|
+
* Keys should match the schema property keys.
|
|
295
|
+
*/
|
|
296
|
+
objectData: {
|
|
297
|
+
type: Object,
|
|
298
|
+
required: true,
|
|
299
|
+
},
|
|
300
|
+
/**
|
|
301
|
+
* The registered object type slug in the objectStore.
|
|
302
|
+
* Required for saving via objectStore.saveObject().
|
|
303
|
+
*/
|
|
304
|
+
objectType: {
|
|
305
|
+
type: String,
|
|
306
|
+
default: '',
|
|
307
|
+
},
|
|
308
|
+
/**
|
|
309
|
+
* Optional objectStore instance. When provided, used directly for saving.
|
|
310
|
+
* When not provided, falls back to auto-detecting the store via Pinia.
|
|
311
|
+
*/
|
|
312
|
+
store: {
|
|
313
|
+
type: Object,
|
|
314
|
+
default: null,
|
|
315
|
+
},
|
|
316
|
+
/**
|
|
317
|
+
* Per-property configuration overrides.
|
|
318
|
+
* Keys are property names, values are override objects.
|
|
319
|
+
*
|
|
320
|
+
* Supported overrides:
|
|
321
|
+
* - `order` (number) — Display order (lower = first)
|
|
322
|
+
* - `gridColumn` (number) — Number of grid columns to span (default 1)
|
|
323
|
+
* - `gridRow` (number) — Number of grid rows to span (default 1)
|
|
324
|
+
* - `hidden` (boolean) — Hide this property
|
|
325
|
+
* - `editable` (boolean) — Override editability (default: based on schema readOnly)
|
|
326
|
+
* - `label` (string) — Override the display label
|
|
327
|
+
* - `widget` (string) — Override the widget type for editing
|
|
328
|
+
*
|
|
329
|
+
* @type {Object<string, { order?: number, gridColumn?: number, gridRow?: number, hidden?: boolean, editable?: boolean, label?: string, widget?: string }>}
|
|
330
|
+
*/
|
|
331
|
+
overrides: {
|
|
332
|
+
type: Object,
|
|
333
|
+
default: () => ({}),
|
|
334
|
+
},
|
|
335
|
+
/**
|
|
336
|
+
* Number of grid columns.
|
|
337
|
+
*/
|
|
338
|
+
columns: {
|
|
339
|
+
type: Number,
|
|
340
|
+
default: 3,
|
|
341
|
+
},
|
|
342
|
+
/**
|
|
343
|
+
* Whether editing is enabled globally.
|
|
344
|
+
* When false, no fields are editable regardless of per-field settings.
|
|
345
|
+
*/
|
|
346
|
+
editable: {
|
|
347
|
+
type: Boolean,
|
|
348
|
+
default: true,
|
|
349
|
+
},
|
|
350
|
+
/**
|
|
351
|
+
* Properties to exclude from display.
|
|
352
|
+
* @type {string[]}
|
|
353
|
+
*/
|
|
354
|
+
exclude: {
|
|
355
|
+
type: Array,
|
|
356
|
+
default: () => [],
|
|
357
|
+
},
|
|
358
|
+
/**
|
|
359
|
+
* Properties to include (whitelist mode). If provided, only these are shown.
|
|
360
|
+
* @type {string[]}
|
|
361
|
+
*/
|
|
362
|
+
include: {
|
|
363
|
+
type: Array,
|
|
364
|
+
default: () => null,
|
|
365
|
+
},
|
|
366
|
+
/** Label for the save button */
|
|
367
|
+
saveLabel: {
|
|
368
|
+
type: String,
|
|
369
|
+
default: 'Save',
|
|
370
|
+
},
|
|
371
|
+
/** Label for the discard button */
|
|
372
|
+
discardLabel: {
|
|
373
|
+
type: String,
|
|
374
|
+
default: 'Discard',
|
|
375
|
+
},
|
|
376
|
+
/** Label shown when no properties to display */
|
|
377
|
+
emptyLabel: {
|
|
378
|
+
type: String,
|
|
379
|
+
default: 'No data available',
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
data() {
|
|
384
|
+
return {
|
|
385
|
+
/** Currently editing field key, or null */
|
|
386
|
+
editingField: null,
|
|
387
|
+
/** Working copy of changed field values */
|
|
388
|
+
editData: {},
|
|
389
|
+
/** Set of field keys that have been modified */
|
|
390
|
+
dirtyFields: {},
|
|
391
|
+
/** Whether a save is in progress */
|
|
392
|
+
saving: false,
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
computed: {
|
|
397
|
+
iconComponent() {
|
|
398
|
+
return this.icon
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Resolved field definitions from schema + overrides.
|
|
403
|
+
* Sorted by order, filtered by hidden/exclude/include.
|
|
404
|
+
*/
|
|
405
|
+
resolvedFields() {
|
|
406
|
+
// Build override map for fieldsFromSchema
|
|
407
|
+
const fieldOverrides = {}
|
|
408
|
+
for (const [key, cfg] of Object.entries(this.overrides)) {
|
|
409
|
+
const override = {}
|
|
410
|
+
if (cfg.label) override.label = cfg.label
|
|
411
|
+
if (cfg.widget) override.widget = cfg.widget
|
|
412
|
+
if (typeof cfg.order === 'number') override.order = cfg.order
|
|
413
|
+
if (cfg.enum) override.enum = cfg.enum
|
|
414
|
+
if (Object.keys(override).length > 0) {
|
|
415
|
+
fieldOverrides[key] = override
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Build exclude list: merge prop exclude + hidden overrides
|
|
420
|
+
const excludeList = [...this.exclude]
|
|
421
|
+
for (const [key, cfg] of Object.entries(this.overrides)) {
|
|
422
|
+
if (cfg.hidden === true && !excludeList.includes(key)) {
|
|
423
|
+
excludeList.push(key)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const fields = fieldsFromSchema(this.schema, {
|
|
428
|
+
exclude: excludeList,
|
|
429
|
+
include: this.include,
|
|
430
|
+
overrides: fieldOverrides,
|
|
431
|
+
includeReadOnly: true,
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
// Attach grid span info from overrides
|
|
435
|
+
const result = fields.map(field => ({
|
|
436
|
+
...field,
|
|
437
|
+
gridColumn: (this.overrides[field.key] && this.overrides[field.key].gridColumn) || 1,
|
|
438
|
+
gridRow: (this.overrides[field.key] && this.overrides[field.key].gridRow) || 1,
|
|
439
|
+
}))
|
|
440
|
+
|
|
441
|
+
// Re-sort by order after overrides are applied
|
|
442
|
+
// (fieldsFromSchema sorts before applying overrides, so order may have changed)
|
|
443
|
+
result.sort(function(a, b) {
|
|
444
|
+
const orderA = typeof a.order === 'number' ? a.order : Infinity
|
|
445
|
+
const orderB = typeof b.order === 'number' ? b.order : Infinity
|
|
446
|
+
if (orderA !== orderB) return orderA - orderB
|
|
447
|
+
return a.key.localeCompare(b.key)
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
return result
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Formatted display values for each field.
|
|
455
|
+
*/
|
|
456
|
+
displayValues() {
|
|
457
|
+
const values = {}
|
|
458
|
+
for (const field of this.resolvedFields) {
|
|
459
|
+
// Show pending edit value if dirty
|
|
460
|
+
const raw = field.key in this.dirtyFields
|
|
461
|
+
? this.dirtyFields[field.key]
|
|
462
|
+
: this.objectData[field.key]
|
|
463
|
+
const prop = this.schema.properties && this.schema.properties[field.key]
|
|
464
|
+
values[field.key] = formatValue(raw, prop || {})
|
|
465
|
+
}
|
|
466
|
+
return values
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Whether any fields have been modified.
|
|
471
|
+
*/
|
|
472
|
+
isDirty() {
|
|
473
|
+
return Object.keys(this.dirtyFields).length > 0
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* CSS grid template for the container.
|
|
478
|
+
*/
|
|
479
|
+
gridStyle() {
|
|
480
|
+
return {
|
|
481
|
+
'grid-template-columns': `repeat(${this.columns}, 1fr)`,
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
watch: {
|
|
487
|
+
objectData: {
|
|
488
|
+
deep: true,
|
|
489
|
+
handler() {
|
|
490
|
+
// If external data changes (e.g. after save), clear dirty state
|
|
491
|
+
// for fields that now match the new data
|
|
492
|
+
for (const key of Object.keys(this.dirtyFields)) {
|
|
493
|
+
if (this.dirtyFields[key] === this.objectData[key]) {
|
|
494
|
+
const { [key]: _, ...rest } = this.dirtyFields
|
|
495
|
+
this.dirtyFields = rest
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
methods: {
|
|
503
|
+
/**
|
|
504
|
+
* Check if a field is editable.
|
|
505
|
+
* @param field
|
|
506
|
+
*/
|
|
507
|
+
isEditable(field) {
|
|
508
|
+
if (!this.editable) return false
|
|
509
|
+
// Per-field override takes priority
|
|
510
|
+
const override = this.overrides[field.key]
|
|
511
|
+
if (override && typeof override.editable === 'boolean') {
|
|
512
|
+
return override.editable
|
|
513
|
+
}
|
|
514
|
+
// Schema readOnly
|
|
515
|
+
return !field.readOnly
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Check if a field's current value is empty.
|
|
520
|
+
* @param key
|
|
521
|
+
*/
|
|
522
|
+
isValueEmpty(key) {
|
|
523
|
+
const val = key in this.dirtyFields
|
|
524
|
+
? this.dirtyFields[key]
|
|
525
|
+
: this.objectData[key]
|
|
526
|
+
return val === null || val === undefined || val === '' || (Array.isArray(val) && val.length === 0)
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Start inline editing for a field.
|
|
531
|
+
* @param field
|
|
532
|
+
*/
|
|
533
|
+
startEdit(field) {
|
|
534
|
+
// Set working value: dirty value > current object value
|
|
535
|
+
const currentValue = field.key in this.dirtyFields
|
|
536
|
+
? this.dirtyFields[field.key]
|
|
537
|
+
: this.objectData[field.key]
|
|
538
|
+
this.editData = { ...this.editData, [field.key]: currentValue }
|
|
539
|
+
this.editingField = field.key
|
|
540
|
+
|
|
541
|
+
this.$nextTick(() => {
|
|
542
|
+
// Focus the editor
|
|
543
|
+
const editor = this.$refs.activeEditor
|
|
544
|
+
if (editor) {
|
|
545
|
+
const el = Array.isArray(editor) ? editor[0] : editor
|
|
546
|
+
if (el && el.$el) {
|
|
547
|
+
const input = el.$el.querySelector('input, textarea, select')
|
|
548
|
+
if (input) input.focus()
|
|
549
|
+
} else if (el && el.focus) {
|
|
550
|
+
el.focus()
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
})
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Update the working edit value for a field.
|
|
558
|
+
* @param key
|
|
559
|
+
* @param value
|
|
560
|
+
*/
|
|
561
|
+
updateField(key, value) {
|
|
562
|
+
this.editData = { ...this.editData, [key]: value }
|
|
563
|
+
},
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Commit the current inline edit — mark the field as dirty.
|
|
567
|
+
*/
|
|
568
|
+
commitEdit() {
|
|
569
|
+
if (!this.editingField) return
|
|
570
|
+
|
|
571
|
+
const key = this.editingField
|
|
572
|
+
const newValue = this.editData[key]
|
|
573
|
+
const originalValue = this.objectData[key]
|
|
574
|
+
|
|
575
|
+
// Only mark dirty if actually changed
|
|
576
|
+
if (newValue !== originalValue) {
|
|
577
|
+
this.dirtyFields = { ...this.dirtyFields, [key]: newValue }
|
|
578
|
+
} else {
|
|
579
|
+
// Remove from dirty if reverted to original
|
|
580
|
+
const { [key]: _, ...rest } = this.dirtyFields
|
|
581
|
+
this.dirtyFields = rest
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
this.editingField = null
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Cancel the current inline edit without saving.
|
|
589
|
+
*/
|
|
590
|
+
cancelEdit() {
|
|
591
|
+
this.editingField = null
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Discard all pending changes.
|
|
596
|
+
*/
|
|
597
|
+
discard() {
|
|
598
|
+
this.dirtyFields = {}
|
|
599
|
+
this.editData = {}
|
|
600
|
+
this.editingField = null
|
|
601
|
+
this.$emit('discard')
|
|
602
|
+
},
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Save all dirty fields via the objectStore or emit event.
|
|
606
|
+
*/
|
|
607
|
+
async save() {
|
|
608
|
+
if (!this.isDirty) return
|
|
609
|
+
|
|
610
|
+
const mergedData = {
|
|
611
|
+
...this.objectData,
|
|
612
|
+
...this.dirtyFields,
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
this.saving = true
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
// Try objectStore first if objectType is registered
|
|
619
|
+
if (this.objectType) {
|
|
620
|
+
const store = this._getObjectStore()
|
|
621
|
+
if (store) {
|
|
622
|
+
const result = await store.saveObject(this.objectType, mergedData)
|
|
623
|
+
if (result) {
|
|
624
|
+
this.dirtyFields = {}
|
|
625
|
+
this.editData = {}
|
|
626
|
+
this.$emit('saved', result)
|
|
627
|
+
} else {
|
|
628
|
+
const error = store.getError(this.objectType)
|
|
629
|
+
this.$emit('save-error', error)
|
|
630
|
+
}
|
|
631
|
+
return
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Fallback: emit for parent to handle
|
|
636
|
+
this.$emit('save', mergedData)
|
|
637
|
+
} finally {
|
|
638
|
+
this.saving = false
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Get the objectStore instance.
|
|
644
|
+
* Uses the `store` prop if provided, otherwise tries Pinia auto-detection.
|
|
645
|
+
* @return {object|null}
|
|
646
|
+
*/
|
|
647
|
+
_getObjectStore() {
|
|
648
|
+
// Prefer explicit store prop
|
|
649
|
+
if (this.store) return this.store
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
// Dynamic import to avoid hard dependency
|
|
653
|
+
const pinia = this.$pinia
|
|
654
|
+
if (!pinia) return null
|
|
655
|
+
// Try to access the store — it must already be created by the consuming app
|
|
656
|
+
const { useObjectStore } = require('../../store/index.js')
|
|
657
|
+
return useObjectStore()
|
|
658
|
+
} catch {
|
|
659
|
+
return null
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Compute CSS grid placement for a field cell.
|
|
665
|
+
* @param field
|
|
666
|
+
*/
|
|
667
|
+
cellStyle(field) {
|
|
668
|
+
const style = {}
|
|
669
|
+
if (field.gridColumn > 1) {
|
|
670
|
+
style.gridColumn = `span ${field.gridColumn}`
|
|
671
|
+
}
|
|
672
|
+
if (field.gridRow > 1) {
|
|
673
|
+
style.gridRow = `span ${field.gridRow}`
|
|
674
|
+
}
|
|
675
|
+
return style
|
|
676
|
+
},
|
|
677
|
+
|
|
678
|
+
// ── Select helpers ──
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Normalize an option to { id, label } format.
|
|
682
|
+
* Accepts plain strings or objects with id/label properties.
|
|
683
|
+
* @param val
|
|
684
|
+
*/
|
|
685
|
+
_normalizeOption(val) {
|
|
686
|
+
if (val && typeof val === 'object' && val.id !== undefined) {
|
|
687
|
+
return { id: val.id, label: val.label || val.id }
|
|
688
|
+
}
|
|
689
|
+
return { id: val, label: String(val) }
|
|
690
|
+
},
|
|
691
|
+
|
|
692
|
+
getSelectOptions(field) {
|
|
693
|
+
if (field.enum) {
|
|
694
|
+
return field.enum.map(val => this._normalizeOption(val))
|
|
695
|
+
}
|
|
696
|
+
return []
|
|
697
|
+
},
|
|
698
|
+
|
|
699
|
+
getSelectedOption(field) {
|
|
700
|
+
const val = this.editData[field.key]
|
|
701
|
+
if (val === null || val === undefined) return null
|
|
702
|
+
// Find matching option from enum for proper label display
|
|
703
|
+
const options = this.getSelectOptions(field)
|
|
704
|
+
return options.find(opt => opt.id === val) || { id: val, label: String(val) }
|
|
705
|
+
},
|
|
706
|
+
|
|
707
|
+
onSelectChange(field, option) {
|
|
708
|
+
this.updateField(field.key, option ? option.id : null)
|
|
709
|
+
},
|
|
710
|
+
|
|
711
|
+
getMultiselectOptions(field) {
|
|
712
|
+
// Check override enum first, then schema items.enum
|
|
713
|
+
if (field.enum) {
|
|
714
|
+
return field.enum.map(val => this._normalizeOption(val))
|
|
715
|
+
}
|
|
716
|
+
const itemsEnum = field.items && field.items.enum
|
|
717
|
+
if (itemsEnum) {
|
|
718
|
+
return itemsEnum.map(val => this._normalizeOption(val))
|
|
719
|
+
}
|
|
720
|
+
return []
|
|
721
|
+
},
|
|
722
|
+
|
|
723
|
+
getSelectedMultiselectOptions(field) {
|
|
724
|
+
const val = this.editData[field.key]
|
|
725
|
+
if (!Array.isArray(val)) return []
|
|
726
|
+
// Map selected IDs to option objects with labels
|
|
727
|
+
const options = this.getMultiselectOptions(field)
|
|
728
|
+
return val.map(v => options.find(opt => opt.id === v) || { id: v, label: String(v) })
|
|
729
|
+
},
|
|
730
|
+
|
|
731
|
+
onMultiselectChange(field, selected) {
|
|
732
|
+
const values = Array.isArray(selected)
|
|
733
|
+
? selected.map(opt => opt.id || opt)
|
|
734
|
+
: []
|
|
735
|
+
this.updateField(field.key, values)
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
}
|
|
739
|
+
</script>
|
|
740
|
+
|
|
741
|
+
<style scoped>
|
|
742
|
+
.cn-object-data-widget__grid {
|
|
743
|
+
display: grid;
|
|
744
|
+
gap: calc(2 * var(--default-grid-baseline, 4px)) calc(4 * var(--default-grid-baseline, 4px));
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.cn-object-data-widget__cell {
|
|
748
|
+
display: flex;
|
|
749
|
+
flex-direction: column;
|
|
750
|
+
gap: 2px;
|
|
751
|
+
padding: calc(2 * var(--default-grid-baseline, 4px)) 0;
|
|
752
|
+
border-bottom: 1px solid var(--color-border-dark);
|
|
753
|
+
min-width: 0;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.cn-object-data-widget__label {
|
|
757
|
+
font-size: 0.85em;
|
|
758
|
+
color: var(--color-text-maxcontrast);
|
|
759
|
+
font-weight: 500;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.cn-object-data-widget__value {
|
|
763
|
+
font-size: 1em;
|
|
764
|
+
color: var(--color-main-text);
|
|
765
|
+
word-break: break-word;
|
|
766
|
+
position: relative;
|
|
767
|
+
padding-right: 20px;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.cn-object-data-widget__value--editable {
|
|
771
|
+
cursor: pointer;
|
|
772
|
+
border-radius: var(--border-radius);
|
|
773
|
+
padding: 4px 24px 4px 4px;
|
|
774
|
+
margin: -4px;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.cn-object-data-widget__value--editable:hover {
|
|
778
|
+
background: var(--color-background-dark);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.cn-object-data-widget__value--editable:focus-visible {
|
|
782
|
+
outline: 2px solid var(--color-primary-element);
|
|
783
|
+
outline-offset: 2px;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.cn-object-data-widget__value--empty {
|
|
787
|
+
color: var(--color-text-maxcontrast);
|
|
788
|
+
font-style: italic;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.cn-object-data-widget__edit-icon {
|
|
792
|
+
position: absolute;
|
|
793
|
+
right: 4px;
|
|
794
|
+
top: 50%;
|
|
795
|
+
transform: translateY(-50%);
|
|
796
|
+
color: var(--color-text-maxcontrast);
|
|
797
|
+
opacity: 0;
|
|
798
|
+
transition: opacity 0.15s ease;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.cn-object-data-widget__value--editable:hover .cn-object-data-widget__edit-icon,
|
|
802
|
+
.cn-object-data-widget__value--editable:focus-visible .cn-object-data-widget__edit-icon {
|
|
803
|
+
opacity: 1;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.cn-object-data-widget__editor {
|
|
807
|
+
display: flex;
|
|
808
|
+
flex-direction: column;
|
|
809
|
+
gap: 4px;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.cn-object-data-widget__editor-actions {
|
|
813
|
+
display: flex;
|
|
814
|
+
gap: 2px;
|
|
815
|
+
justify-content: flex-end;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.cn-object-data-widget__textarea {
|
|
819
|
+
width: 100%;
|
|
820
|
+
min-height: 80px;
|
|
821
|
+
padding: 8px;
|
|
822
|
+
border: 2px solid var(--color-border-dark);
|
|
823
|
+
border-radius: var(--border-radius);
|
|
824
|
+
background: var(--color-main-background);
|
|
825
|
+
color: var(--color-main-text);
|
|
826
|
+
font-family: inherit;
|
|
827
|
+
font-size: inherit;
|
|
828
|
+
resize: vertical;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.cn-object-data-widget__textarea:focus {
|
|
832
|
+
border-color: var(--color-primary-element);
|
|
833
|
+
outline: none;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.cn-object-data-widget__empty {
|
|
837
|
+
color: var(--color-text-maxcontrast);
|
|
838
|
+
font-style: italic;
|
|
839
|
+
padding: calc(2 * var(--default-grid-baseline, 4px));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/* Responsive: collapse to single column on narrow widths */
|
|
843
|
+
@media (max-width: 600px) {
|
|
844
|
+
.cn-object-data-widget__grid {
|
|
845
|
+
grid-template-columns: 1fr !important;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
.cn-object-data-widget__cell {
|
|
849
|
+
grid-column: span 1 !important;
|
|
850
|
+
grid-row: span 1 !important;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
</style>
|