@conduction/nextcloud-vue 0.1.0-beta.17 → 0.1.0-beta.18
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 +524 -317
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +64 -56
- package/dist/nextcloud-vue.esm.js +524 -317
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/l10n/en.json +2 -0
- package/l10n/nl.json +2 -0
- package/package.json +1 -1
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +135 -118
- package/src/components/CnDashboardPage/CnDashboardPage.vue +17 -1
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +1 -1
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +32 -0
- package/src/css/index.css +1 -0
- package/src/css/main.css +3 -0
- package/src/store/plugins/files.js +53 -1
package/l10n/en.json
CHANGED
|
@@ -370,6 +370,7 @@
|
|
|
370
370
|
"The 'admin' group always has full access (cannot be changed)": "The 'admin' group always has full access (cannot be changed)",
|
|
371
371
|
"The following items will be permanently deleted. Remove any items you want to keep.": "The following items will be permanently deleted. Remove any items you want to keep.",
|
|
372
372
|
"The object owner always has full access": "The object owner always has full access",
|
|
373
|
+
"This field has been changed — save to apply": "This field has been changed — save to apply",
|
|
373
374
|
"Time": "Time",
|
|
374
375
|
"Title": "Title",
|
|
375
376
|
"Title *": "Title *",
|
|
@@ -402,6 +403,7 @@
|
|
|
402
403
|
"Widget not available": "Widget not available",
|
|
403
404
|
"Write a note...": "Write a note...",
|
|
404
405
|
"Write back": "Write back",
|
|
406
|
+
"You have unsaved changes. Save to apply them.": "You have unsaved changes. Save to apply them.",
|
|
405
407
|
"{name} (Copy)": "{name} (Copy)",
|
|
406
408
|
"{name} - Copy": "{name} - Copy",
|
|
407
409
|
"{title} saved successfully.": "{title} saved successfully.",
|
package/l10n/nl.json
CHANGED
|
@@ -368,6 +368,7 @@
|
|
|
368
368
|
"The 'admin' group always has full access (cannot be changed)": "De 'admin'-groep heeft altijd volledige toegang (kan niet worden gewijzigd)",
|
|
369
369
|
"The following items will be permanently deleted. Remove any items you want to keep.": "De volgende items worden permanent verwijderd. Haal items die u wilt bewaren uit de selectie.",
|
|
370
370
|
"The object owner always has full access": "De eigenaar van het object heeft altijd volledige toegang",
|
|
371
|
+
"This field has been changed — save to apply": "Dit veld is gewijzigd — sla op om toe te passen",
|
|
371
372
|
"Time": "Tijd",
|
|
372
373
|
"Title": "Titel",
|
|
373
374
|
"Title *": "Titel *",
|
|
@@ -400,6 +401,7 @@
|
|
|
400
401
|
"Widget not available": "Widget niet beschikbaar",
|
|
401
402
|
"Write a note...": "Schrijf een notitie...",
|
|
402
403
|
"Write back": "Terugschrijven",
|
|
404
|
+
"You have unsaved changes. Save to apply them.": "U heeft niet-opgeslagen wijzigingen. Sla op om ze toe te passen.",
|
|
403
405
|
"{name} (Copy)": "{name} (Kopie)",
|
|
404
406
|
"{name} - Copy": "{name} - Kopie",
|
|
405
407
|
"{title} saved successfully.": "{title} succesvol opgeslagen.",
|
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.18",
|
|
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>",
|
|
@@ -1,125 +1,135 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
[
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
:editability-warning="getPropertyEditabilityWarning(key, resolvedValue(key, value))"
|
|
84
|
-
:on-update="(v) => onPropertyValueUpdate(key, v)">
|
|
85
|
-
<CnPropertyValueCell
|
|
86
|
-
:ref="'cell-' + key"
|
|
2
|
+
<div>
|
|
3
|
+
<NcNoteCard
|
|
4
|
+
v-if="hasUnsavedChanges"
|
|
5
|
+
type="warning"
|
|
6
|
+
class="cn-advanced-form-dialog__unsaved-note">
|
|
7
|
+
{{ t('nextcloud-vue', 'You have unsaved changes. Save to apply them.') }}
|
|
8
|
+
</NcNoteCard>
|
|
9
|
+
<div class="cn-advanced-form-dialog__table-container">
|
|
10
|
+
<table class="cn-advanced-form-dialog__table">
|
|
11
|
+
<thead>
|
|
12
|
+
<tr class="cn-advanced-form-dialog__table-row">
|
|
13
|
+
<th class="cn-advanced-form-dialog__table-col-constrained">
|
|
14
|
+
{{ t('nextcloud-vue', 'Property') }}
|
|
15
|
+
</th>
|
|
16
|
+
<th class="cn-advanced-form-dialog__table-col-expanded">
|
|
17
|
+
{{ t('nextcloud-vue', 'Value') }}
|
|
18
|
+
</th>
|
|
19
|
+
<th
|
|
20
|
+
v-if="hasRowActionsSlot"
|
|
21
|
+
class="cn-advanced-form-dialog__table-col-actions">
|
|
22
|
+
<slot name="row-actions-header" />
|
|
23
|
+
</th>
|
|
24
|
+
</tr>
|
|
25
|
+
</thead>
|
|
26
|
+
<tbody>
|
|
27
|
+
<tr
|
|
28
|
+
v-for="([key, value]) in objectProperties"
|
|
29
|
+
:key="key"
|
|
30
|
+
class="cn-advanced-form-dialog__table-row"
|
|
31
|
+
:class="{
|
|
32
|
+
'cn-advanced-form-dialog__table-row--selected': selectedProperty === key,
|
|
33
|
+
'cn-advanced-form-dialog__table-row--edited': isValueChanged(key),
|
|
34
|
+
'cn-advanced-form-dialog__table-row--non-editable': !isPropertyEditable(key, resolvedValue(key, value)),
|
|
35
|
+
[getPropertyValidationClass(key, value)]: validationDisplay === 'indicator',
|
|
36
|
+
}"
|
|
37
|
+
@click="handleRowClick(key, $event)">
|
|
38
|
+
<td class="cn-advanced-form-dialog__table-col-constrained cn-advanced-form-dialog__prop-cell"
|
|
39
|
+
:style="getPropCellStyle(key, value)">
|
|
40
|
+
<div class="cn-advanced-form-dialog__prop-cell-content">
|
|
41
|
+
<AlertCircle
|
|
42
|
+
v-if="validationDisplay === 'indicator' && getPropertyValidationState(key, resolvedValue(key, value)) === 'invalid'"
|
|
43
|
+
class="cn-advanced-form-dialog__validation-icon cn-advanced-form-dialog__validation-icon--error"
|
|
44
|
+
:size="16"
|
|
45
|
+
:title="getPropertyErrorMessage(key, resolvedValue(key, value))" />
|
|
46
|
+
<PencilOutline
|
|
47
|
+
v-else-if="validationDisplay === 'indicator' && isValueChanged(key) && getPropertyValidationState(key, resolvedValue(key, value)) !== 'invalid'"
|
|
48
|
+
v-tooltip="t('nextcloud-vue', 'This field has been changed — save to apply')"
|
|
49
|
+
class="cn-advanced-form-dialog__validation-icon cn-advanced-form-dialog__validation-icon--edited"
|
|
50
|
+
:size="16" />
|
|
51
|
+
<Alert
|
|
52
|
+
v-else-if="validationDisplay === 'indicator' && getPropertyValidationState(key, resolvedValue(key, value)) === 'warning'"
|
|
53
|
+
class="cn-advanced-form-dialog__validation-icon cn-advanced-form-dialog__validation-icon--warning"
|
|
54
|
+
:size="16"
|
|
55
|
+
:title="getPropertyWarningMessage(key, resolvedValue(key, value))" />
|
|
56
|
+
<Plus
|
|
57
|
+
v-else-if="validationDisplay === 'indicator' && getPropertyValidationState(key, resolvedValue(key, value)) === 'new'"
|
|
58
|
+
class="cn-advanced-form-dialog__validation-icon cn-advanced-form-dialog__validation-icon--new"
|
|
59
|
+
:size="16"
|
|
60
|
+
:title="getPropertyNewMessage(key)" />
|
|
61
|
+
<LockOutline
|
|
62
|
+
v-else-if="!isPropertyEditable(key, resolvedValue(key, value))"
|
|
63
|
+
class="cn-advanced-form-dialog__validation-icon cn-advanced-form-dialog__validation-icon--lock"
|
|
64
|
+
:size="16"
|
|
65
|
+
:title="getEditabilityWarning(key, resolvedValue(key, value)) || ''" />
|
|
66
|
+
<span :title="getPropertyTooltip(key)">{{ getPropertyDisplayName(key) }}</span>
|
|
67
|
+
<span
|
|
68
|
+
v-if="isRequired(key)"
|
|
69
|
+
class="cn-advanced-form-dialog__required-indicator"
|
|
70
|
+
:title="t('nextcloud-vue', 'Required')"
|
|
71
|
+
aria-label="required">*</span>
|
|
72
|
+
<span
|
|
73
|
+
v-if="isImmutableHint(key)"
|
|
74
|
+
class="cn-advanced-form-dialog__immutable-badge"
|
|
75
|
+
:title="t('nextcloud-vue', 'This value can be set on creation but cannot be changed afterwards.')">
|
|
76
|
+
{{ t('nextcloud-vue', 'Set once') }}
|
|
77
|
+
</span>
|
|
78
|
+
</div>
|
|
79
|
+
</td>
|
|
80
|
+
<td class="cn-advanced-form-dialog__table-col-expanded cn-advanced-form-dialog__value-cell">
|
|
81
|
+
<slot
|
|
82
|
+
name="value-cell"
|
|
87
83
|
:property-key="key"
|
|
88
|
-
:
|
|
89
|
-
:value="resolvedValue(key, value)"
|
|
90
|
-
:is-editable="isPropertyEditable(key, resolvedValue(key, value))"
|
|
84
|
+
:value="value"
|
|
85
|
+
:resolved-value="resolvedValue(key, value)"
|
|
91
86
|
:is-editing="selectedProperty === key"
|
|
87
|
+
:is-editable="isPropertyEditable(key, resolvedValue(key, value))"
|
|
92
88
|
:display-name="getPropertyDisplayName(key)"
|
|
89
|
+
:schema-prop="schema && schema.properties && schema.properties[key]"
|
|
93
90
|
:editability-warning="getPropertyEditabilityWarning(key, resolvedValue(key, value))"
|
|
94
|
-
:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
91
|
+
:on-update="(v) => onPropertyValueUpdate(key, v)">
|
|
92
|
+
<CnPropertyValueCell
|
|
93
|
+
:ref="'cell-' + key"
|
|
94
|
+
:property-key="key"
|
|
95
|
+
:schema="schema"
|
|
96
|
+
:value="resolvedValue(key, value)"
|
|
97
|
+
:is-editable="isPropertyEditable(key, resolvedValue(key, value))"
|
|
98
|
+
:is-editing="selectedProperty === key"
|
|
99
|
+
:display-name="getPropertyDisplayName(key)"
|
|
100
|
+
:editability-warning="getPropertyEditabilityWarning(key, resolvedValue(key, value))"
|
|
101
|
+
:widget="(propertyOverrides[key] && propertyOverrides[key].widget) || null"
|
|
102
|
+
:select-options="(propertyOverrides[key] && propertyOverrides[key].selectOptions) || null"
|
|
103
|
+
:select-multiple="propertyOverrides[key] ? propertyOverrides[key].selectMultiple !== false : true"
|
|
104
|
+
:textarea-rows="(propertyOverrides[key] && propertyOverrides[key].textareaRows) || 4"
|
|
105
|
+
@update:value="onPropertyValueUpdate(key, $event)" />
|
|
106
|
+
</slot>
|
|
107
|
+
</td>
|
|
108
|
+
<td
|
|
109
|
+
v-if="hasRowActionsSlot"
|
|
110
|
+
class="cn-advanced-form-dialog__table-col-actions"
|
|
111
|
+
@click.stop>
|
|
112
|
+
<slot
|
|
113
|
+
name="row-actions"
|
|
114
|
+
:property-key="key"
|
|
115
|
+
:value="value"
|
|
116
|
+
:resolved-value="resolvedValue(key, value)"
|
|
117
|
+
:is-editable="isPropertyEditable(key, resolvedValue(key, value))"
|
|
118
|
+
:is-schema-property="!!(schema && schema.properties && Object.prototype.hasOwnProperty.call(schema.properties, key))" />
|
|
119
|
+
</td>
|
|
120
|
+
</tr>
|
|
121
|
+
</tbody>
|
|
122
|
+
</table>
|
|
123
|
+
</div>
|
|
116
124
|
</div>
|
|
117
125
|
</template>
|
|
118
126
|
|
|
119
127
|
<script>
|
|
120
128
|
import { translate as t } from '@nextcloud/l10n'
|
|
129
|
+
import { NcNoteCard } from '@nextcloud/vue'
|
|
121
130
|
import AlertCircle from 'vue-material-design-icons/AlertCircle.vue'
|
|
122
131
|
import Alert from 'vue-material-design-icons/Alert.vue'
|
|
132
|
+
import PencilOutline from 'vue-material-design-icons/PencilOutline.vue'
|
|
123
133
|
import LockOutline from 'vue-material-design-icons/LockOutline.vue'
|
|
124
134
|
import Plus from 'vue-material-design-icons/Plus.vue'
|
|
125
135
|
import CnPropertyValueCell from './CnPropertyValueCell.vue'
|
|
@@ -128,8 +138,10 @@ export default {
|
|
|
128
138
|
name: 'CnPropertiesTab',
|
|
129
139
|
|
|
130
140
|
components: {
|
|
141
|
+
NcNoteCard,
|
|
131
142
|
AlertCircle,
|
|
132
143
|
Alert,
|
|
144
|
+
PencilOutline,
|
|
133
145
|
LockOutline,
|
|
134
146
|
Plus,
|
|
135
147
|
CnPropertyValueCell,
|
|
@@ -166,9 +178,14 @@ export default {
|
|
|
166
178
|
* Any other CSS color string is applied directly.
|
|
167
179
|
*/
|
|
168
180
|
propCellColor: { type: String, default: null },
|
|
181
|
+
isNew: { type: Boolean, default: false },
|
|
169
182
|
},
|
|
170
183
|
|
|
171
184
|
computed: {
|
|
185
|
+
hasUnsavedChanges() {
|
|
186
|
+
if (this.isNew) return false
|
|
187
|
+
return Object.keys(this.formData).some(key => this.isValueChanged(key))
|
|
188
|
+
},
|
|
172
189
|
propCellStyle() {
|
|
173
190
|
if (this.propCellColor === null) return undefined
|
|
174
191
|
if (this.propCellColor === 'none') return { boxShadow: 'none' }
|
|
@@ -255,7 +272,7 @@ export default {
|
|
|
255
272
|
return { boxShadow: 'inset 3px 0 0 0 var(--color-error)' }
|
|
256
273
|
}
|
|
257
274
|
if (this.isValueChanged(key)) {
|
|
258
|
-
return { boxShadow: 'inset 3px 0 0 0 var(--color-
|
|
275
|
+
return { boxShadow: 'inset 3px 0 0 0 var(--color-primary-element)' }
|
|
259
276
|
}
|
|
260
277
|
if (state === 'valid') {
|
|
261
278
|
return { boxShadow: 'inset 3px 0 0 0 var(--color-success)' }
|
|
@@ -264,7 +281,7 @@ export default {
|
|
|
264
281
|
return { boxShadow: 'inset 3px 0 0 0 var(--color-warning)' }
|
|
265
282
|
}
|
|
266
283
|
if (state === 'new') {
|
|
267
|
-
return { boxShadow: 'inset 3px 0 0 0 var(--color-
|
|
284
|
+
return { boxShadow: 'inset 3px 0 0 0 var(--color-new)' }
|
|
268
285
|
}
|
|
269
286
|
return this.propCellStyle
|
|
270
287
|
},
|
|
@@ -590,10 +607,6 @@ export default {
|
|
|
590
607
|
box-shadow: inset 3px 0 0 0 var(--color-primary);
|
|
591
608
|
}
|
|
592
609
|
|
|
593
|
-
.cn-advanced-form-dialog__table-row--edited {
|
|
594
|
-
background-color: var(--color-warning-light);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
610
|
.cn-advanced-form-dialog__table-row--non-editable {
|
|
598
611
|
background-color: var(--color-background-dark);
|
|
599
612
|
cursor: not-allowed;
|
|
@@ -661,7 +674,7 @@ export default {
|
|
|
661
674
|
}
|
|
662
675
|
|
|
663
676
|
.cn-advanced-form-dialog__validation-icon--edited {
|
|
664
|
-
color: var(--color-
|
|
677
|
+
color: var(--color-primary-element);
|
|
665
678
|
}
|
|
666
679
|
|
|
667
680
|
.cn-advanced-form-dialog__validation-icon--warning {
|
|
@@ -673,7 +686,7 @@ export default {
|
|
|
673
686
|
}
|
|
674
687
|
|
|
675
688
|
.cn-advanced-form-dialog__validation-icon--new {
|
|
676
|
-
color: var(--color-
|
|
689
|
+
color: var(--color-new);
|
|
677
690
|
}
|
|
678
691
|
|
|
679
692
|
.cn-advanced-form-dialog__required-indicator {
|
|
@@ -695,4 +708,8 @@ export default {
|
|
|
695
708
|
cursor: help;
|
|
696
709
|
white-space: nowrap;
|
|
697
710
|
}
|
|
711
|
+
|
|
712
|
+
.cn-advanced-form-dialog__unsaved-note {
|
|
713
|
+
margin-bottom: calc(3 * var(--default-grid-baseline));
|
|
714
|
+
}
|
|
698
715
|
</style>
|
|
@@ -83,7 +83,13 @@
|
|
|
83
83
|
:borderless="item.showTitle === false"
|
|
84
84
|
:flush="item.flush === true"
|
|
85
85
|
:buttons="getWidgetButtons(item)"
|
|
86
|
-
:style-config="item.styleConfig || {}"
|
|
86
|
+
:style-config="item.styleConfig || {}"
|
|
87
|
+
:title-icon-position="getWidgetTitleIconPosition(item)"
|
|
88
|
+
:title-icon-color="getWidgetTitleIconColor(item)">
|
|
89
|
+
<!-- Per-widget title icon (e.g. #widget-my-work-title-icon) -->
|
|
90
|
+
<template v-if="$slots['widget-' + item.widgetId + '-title-icon']" #title-icon>
|
|
91
|
+
<slot :name="'widget-' + item.widgetId + '-title-icon'" :item="item" :widget="getWidgetDef(item.widgetId)" />
|
|
92
|
+
</template>
|
|
87
93
|
<!-- Per-widget header actions (e.g. #widget-my-work-actions) -->
|
|
88
94
|
<template v-if="$slots['widget-' + item.widgetId + '-actions']" #actions>
|
|
89
95
|
<slot :name="'widget-' + item.widgetId + '-actions'" :item="item" :widget="getWidgetDef(item.widgetId)" />
|
|
@@ -320,6 +326,16 @@ export default {
|
|
|
320
326
|
return def?.buttons || []
|
|
321
327
|
},
|
|
322
328
|
|
|
329
|
+
getWidgetTitleIconPosition(item) {
|
|
330
|
+
const def = this.getWidgetDef(item.widgetId)
|
|
331
|
+
return def?.titleIconPosition || 'right'
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
getWidgetTitleIconColor(item) {
|
|
335
|
+
const def = this.getWidgetDef(item.widgetId)
|
|
336
|
+
return def?.titleIconColor || null
|
|
337
|
+
},
|
|
338
|
+
|
|
323
339
|
isTile(item) {
|
|
324
340
|
const def = this.getWidgetDef(item.widgetId)
|
|
325
341
|
return def?.type === 'tile'
|
|
@@ -128,7 +128,7 @@ export default {
|
|
|
128
128
|
computed: {
|
|
129
129
|
itemName() {
|
|
130
130
|
if (this.nameFormatter) return this.nameFormatter(this.item)
|
|
131
|
-
return this.item[this.nameField] || this.item.name || this.item.title || this.item.id
|
|
131
|
+
return this.item[this.nameField] || this.item.name || this.item.naam || this.item.title || this.item.id
|
|
132
132
|
},
|
|
133
133
|
resolvedWarningText() {
|
|
134
134
|
return this.warningText.replace('{name}', this.itemName)
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
:style="wrapperStyles">
|
|
16
16
|
<!-- Header -->
|
|
17
17
|
<div v-if="showTitle" class="cn-widget-wrapper__header">
|
|
18
|
+
<!-- Title icon — left: rendered before the title group -->
|
|
19
|
+
<div v-if="$slots['title-icon'] && titleIconPosition === 'left'"
|
|
20
|
+
class="cn-widget-wrapper__title-icon"
|
|
21
|
+
:style="titleIconColor ? { color: titleIconColor } : {}">
|
|
22
|
+
<slot name="title-icon" />
|
|
23
|
+
</div>
|
|
18
24
|
<div class="cn-widget-wrapper__header-left">
|
|
19
25
|
<img
|
|
20
26
|
v-if="iconUrl"
|
|
@@ -32,6 +38,12 @@
|
|
|
32
38
|
<div class="cn-widget-wrapper__actions">
|
|
33
39
|
<slot name="actions" />
|
|
34
40
|
</div>
|
|
41
|
+
<!-- Title icon — right: rendered after actions, far right -->
|
|
42
|
+
<div v-if="$slots['title-icon'] && titleIconPosition === 'right'"
|
|
43
|
+
class="cn-widget-wrapper__title-icon"
|
|
44
|
+
:style="titleIconColor ? { color: titleIconColor } : {}">
|
|
45
|
+
<slot name="title-icon" />
|
|
46
|
+
</div>
|
|
35
47
|
</div>
|
|
36
48
|
|
|
37
49
|
<!-- Content -->
|
|
@@ -114,6 +126,20 @@ export default {
|
|
|
114
126
|
type: String,
|
|
115
127
|
default: null,
|
|
116
128
|
},
|
|
129
|
+
/**
|
|
130
|
+
* Position of the title-icon slot in the header.
|
|
131
|
+
* 'left' places it before the title; 'right' places it after the actions.
|
|
132
|
+
*/
|
|
133
|
+
titleIconPosition: {
|
|
134
|
+
type: String,
|
|
135
|
+
default: 'right',
|
|
136
|
+
validator: (v) => ['left', 'right'].includes(v),
|
|
137
|
+
},
|
|
138
|
+
/** CSS color value applied to the title-icon slot container */
|
|
139
|
+
titleIconColor: {
|
|
140
|
+
type: String,
|
|
141
|
+
default: null,
|
|
142
|
+
},
|
|
117
143
|
/** Footer action buttons: [{ text, link }] */
|
|
118
144
|
buttons: {
|
|
119
145
|
type: Array,
|
|
@@ -227,6 +253,12 @@ export default {
|
|
|
227
253
|
flex-shrink: 0;
|
|
228
254
|
}
|
|
229
255
|
|
|
256
|
+
.cn-widget-wrapper__title-icon {
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
flex-shrink: 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
230
262
|
.cn-widget-wrapper__footer {
|
|
231
263
|
display: flex;
|
|
232
264
|
justify-content: flex-end;
|
package/src/css/index.css
CHANGED
package/src/css/main.css
ADDED
|
@@ -9,7 +9,7 @@ import { parseResponseError, networkError } from '../../utils/errors.js'
|
|
|
9
9
|
* upload (multipart), publish, unpublish, and delete.
|
|
10
10
|
*
|
|
11
11
|
* State: files, filesLoading, filesError, tags, tagsLoading, tagsError
|
|
12
|
-
* Actions: fetchFiles, uploadFiles, publishFile, unpublishFile, deleteFile, clearFiles, fetchTags
|
|
12
|
+
* Actions: fetchFiles, uploadFiles, publishFile, unpublishFile, deleteFile, batchFiles, clearFiles, fetchTags
|
|
13
13
|
* Getters: getFiles, isFilesLoading, getFilesError, getTags, isTagsLoading, getTagsError
|
|
14
14
|
*
|
|
15
15
|
* @param {object} [options={}] Plugin options
|
|
@@ -245,6 +245,58 @@ export function filesPlugin(options = {}) {
|
|
|
245
245
|
this.filesLoading = false
|
|
246
246
|
}
|
|
247
247
|
},
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Apply a batch action across multiple files in ONE request.
|
|
251
|
+
*
|
|
252
|
+
* Replaces the N-sequential-call pattern (loop calling
|
|
253
|
+
* publishFile/unpublishFile/deleteFile per id) with a single POST
|
|
254
|
+
* to /files/batch. The backend returns 200 when every operation
|
|
255
|
+
* succeeds, or 207 (multi-status) when some fail; the per-file
|
|
256
|
+
* outcomes live in `data.results` and the aggregate counts in
|
|
257
|
+
* `data.summary` (`{ succeeded, failed, total }`).
|
|
258
|
+
*
|
|
259
|
+
* @param {string} type The registered object type slug
|
|
260
|
+
* @param {string} objectId The parent object ID
|
|
261
|
+
* @param {('publish'|'depublish'|'delete'|'label')} action The batch action to apply
|
|
262
|
+
* @param {(string|number)[]} fileIds File IDs to act on (max 100, validated server-side)
|
|
263
|
+
* @param {object} [params={}] Action-specific parameters (e.g. labels for the 'label' action)
|
|
264
|
+
* @return {Promise<object|null>} Response body `{ results, summary }`, or null on transport error
|
|
265
|
+
*/
|
|
266
|
+
async batchFiles(type, objectId, action, fileIds, params = {}) {
|
|
267
|
+
this.filesLoading = true
|
|
268
|
+
this.filesError = null
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const url = this._buildUrl(type, objectId) + '/files/batch'
|
|
272
|
+
|
|
273
|
+
const response = await fetch(url, {
|
|
274
|
+
method: 'POST',
|
|
275
|
+
headers: buildHeaders(),
|
|
276
|
+
body: JSON.stringify({
|
|
277
|
+
action,
|
|
278
|
+
fileIds,
|
|
279
|
+
...params,
|
|
280
|
+
}),
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// 200 = all succeeded, 207 = partial success — both are
|
|
284
|
+
// valid responses; the caller inspects data.summary.
|
|
285
|
+
if (!response.ok && response.status !== 207) {
|
|
286
|
+
this.filesError = await parseResponseError(response, 'files')
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const data = await response.json()
|
|
291
|
+
await this.fetchFiles(type, objectId)
|
|
292
|
+
return data
|
|
293
|
+
} catch (error) {
|
|
294
|
+
this.filesError = networkError(error)
|
|
295
|
+
return null
|
|
296
|
+
} finally {
|
|
297
|
+
this.filesLoading = false
|
|
298
|
+
}
|
|
299
|
+
},
|
|
248
300
|
},
|
|
249
301
|
}
|
|
250
302
|
}
|