@conduction/nextcloud-vue 2.56.0-beta.31 → 2.56.0-beta.32
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/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +55 -4
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +51 -15
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +13 -2
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +63 -52
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js +10 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js +252 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js +140 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/index.js +26 -0
- package/dist/esm/components/CnChipsFieldWidget/index.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js +10 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js +354 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js +172 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/index.js +27 -0
- package/dist/esm/components/CnChoicePickerWidget/index.js.map +1 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +25 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +3 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +29 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +20 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +7 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +77 -3
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +282 -69
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +0 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +65 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +10 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +174 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +75 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/index.js +28 -0
- package/dist/esm/components/CnRadioCardsWidget/index.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +18 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +34 -25
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js +10 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js +222 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js +64 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/index.js +30 -0
- package/dist/esm/components/CnSavedViewsWidget/index.js.map +1 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +10 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +347 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +184 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/index.js +27 -0
- package/dist/esm/components/CnSettingsFormWidget/index.js.map +1 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +87 -6
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +10 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +159 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js +60 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/index.js +26 -0
- package/dist/esm/components/CnSwitchRowsWidget/index.js.map +1 -0
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +47 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +16 -14
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +80 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +75 -32
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +10 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +269 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js +106 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/index.js +27 -0
- package/dist/esm/components/CnToggleTableWidget/index.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +4 -2
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +1 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +1 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +843 -4
- package/dist/esm/utils/contentHeaderLink.js +51 -0
- package/dist/esm/utils/contentHeaderLink.js.map +1 -0
- package/dist/esm/utils/fetchFilterCounts.js +40 -5
- package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
- package/dist/esm/utils/initials.js +32 -0
- package/dist/esm/utils/initials.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +7 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/objectWrite.js +59 -0
- package/dist/esm/utils/objectWrite.js.map +1 -0
- package/dist/esm/utils/widgetDispatch.js +1 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +10510 -6830
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +843 -4
- package/l10n/en.json +1 -0
- package/l10n/nl.json +1 -0
- package/package.json +1 -1
- package/src/components/CnBannerWidget/CnBannerWidget.vue +93 -13
- package/src/components/CnCellRenderer/CnCellRenderer.md +2 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +43 -3
- package/src/components/CnChipsFieldWidget/CnChipsFieldWidget.vue +433 -0
- package/src/components/CnChipsFieldWidget/index.js +25 -0
- package/src/components/CnChoicePickerWidget/CnChoicePickerWidget.vue +556 -0
- package/src/components/CnChoicePickerWidget/index.js +26 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +38 -2
- package/src/components/CnDataTable/CnDataTable.vue +2 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +45 -2
- package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -3
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +265 -4
- package/src/components/CnIndexPage/CnIndexPage.vue +0 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +69 -2
- package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +313 -0
- package/src/components/CnRadioCardsWidget/index.js +27 -0
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +43 -1
- package/src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue +296 -0
- package/src/components/CnSavedViewsWidget/index.js +29 -0
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +13 -0
- package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +555 -0
- package/src/components/CnSettingsFormWidget/index.js +26 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +87 -6
- package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +226 -0
- package/src/components/CnSwitchRowsWidget/index.js +25 -0
- package/src/components/CnTabsWidget/CnTabsWidget.vue +82 -3
- package/src/components/CnTasksWidget/CnTasksWidget.vue +175 -15
- package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +386 -0
- package/src/components/CnToggleTableWidget/index.js +26 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +28 -1
- package/src/components/index.js +7 -0
- package/src/css/look-board-detail.css +36 -2
- package/src/css/look-board-index.css +23 -0
- package/src/css/look-board.css +72 -0
- package/src/index.js +7 -0
- package/src/utils/contentHeaderLink.js +48 -0
- package/src/utils/fetchFilterCounts.js +39 -4
- package/src/utils/initials.js +29 -0
- package/src/utils/libraryWidgetKeys.js +7 -0
- package/src/utils/objectWrite.js +55 -0
- package/src/utils/widgetDispatch.js +1 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { resolveComponent, openBlock, createElementBlock, withModifiers, createElementVNode, normalizeClass, Fragment, renderList, createBlock, withCtx, createTextVNode, toDisplayString, createCommentVNode, withDirectives, vModelSelect, vModelText, vModelDynamic, createVNode } from 'vue';
|
|
2
|
+
|
|
3
|
+
const _hoisted_1 = ["data-testid"];
|
|
4
|
+
const _hoisted_2 = ["for"];
|
|
5
|
+
const _hoisted_3 = ["data-testid"];
|
|
6
|
+
const _hoisted_4 = ["id"];
|
|
7
|
+
const _hoisted_5 = ["id", "onUpdate:modelValue", "disabled"];
|
|
8
|
+
const _hoisted_6 = ["value"];
|
|
9
|
+
const _hoisted_7 = ["id", "onUpdate:modelValue", "rows", "placeholder", "readonly"];
|
|
10
|
+
const _hoisted_8 = ["id", "onUpdate:modelValue", "type", "min", "max", "placeholder", "readonly", "aria-invalid"];
|
|
11
|
+
const _hoisted_9 = {
|
|
12
|
+
key: 2,
|
|
13
|
+
class: "cn-settings-form__error",
|
|
14
|
+
role: "alert"
|
|
15
|
+
};
|
|
16
|
+
const _hoisted_10 = {
|
|
17
|
+
key: 3,
|
|
18
|
+
class: "cn-settings-form__hint"
|
|
19
|
+
};
|
|
20
|
+
const _hoisted_11 = {
|
|
21
|
+
key: 1,
|
|
22
|
+
class: "cn-settings-form__actions"
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
26
|
+
const _component_NcCheckboxRadioSwitch = resolveComponent("NcCheckboxRadioSwitch");
|
|
27
|
+
const _component_NcButton = resolveComponent("NcButton");
|
|
28
|
+
const _component_NcNoteCard = resolveComponent("NcNoteCard");
|
|
29
|
+
|
|
30
|
+
return (openBlock(), createElementBlock("form", {
|
|
31
|
+
class: "cn-settings-form",
|
|
32
|
+
"data-testid": "cn-settings-form",
|
|
33
|
+
novalidate: "",
|
|
34
|
+
onSubmit: _cache[0] || (_cache[0] = withModifiers((...args) => ($options.save && $options.save(...args)), ["prevent"]))
|
|
35
|
+
}, [
|
|
36
|
+
createElementVNode("div", {
|
|
37
|
+
class: normalizeClass(["cn-settings-form__grid", `cn-settings-form__grid--${$options.columns}`])
|
|
38
|
+
}, [
|
|
39
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList($options.fields, (field) => {
|
|
40
|
+
return (openBlock(), createElementBlock("div", {
|
|
41
|
+
key: field.key,
|
|
42
|
+
class: normalizeClass(["cn-settings-form__field", { 'cn-settings-form__field--wide': field.wide }]),
|
|
43
|
+
"data-testid": `cn-settings-form-field-${field.key}`
|
|
44
|
+
}, [
|
|
45
|
+
(field.type === 'switch')
|
|
46
|
+
? (openBlock(), createBlock(_component_NcCheckboxRadioSwitch, {
|
|
47
|
+
key: 0,
|
|
48
|
+
modelValue: $data.form[field.key],
|
|
49
|
+
"onUpdate:modelValue": $event => (($data.form[field.key]) = $event),
|
|
50
|
+
type: "switch",
|
|
51
|
+
disabled: field.readOnly
|
|
52
|
+
}, {
|
|
53
|
+
default: withCtx(() => [
|
|
54
|
+
createTextVNode(toDisplayString(field.label), 1 /* TEXT */)
|
|
55
|
+
]),
|
|
56
|
+
_: 2 /* DYNAMIC */
|
|
57
|
+
}, 1032 /* PROPS, DYNAMIC_SLOTS */, ["modelValue", "onUpdate:modelValue", "disabled"]))
|
|
58
|
+
: (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
59
|
+
createElementVNode("label", {
|
|
60
|
+
class: "cn-settings-form__label",
|
|
61
|
+
for: $options.inputId(field)
|
|
62
|
+
}, toDisplayString(field.label), 9 /* TEXT, PROPS */, _hoisted_2),
|
|
63
|
+
(field.type === 'file')
|
|
64
|
+
? (openBlock(), createElementBlock("div", {
|
|
65
|
+
key: 0,
|
|
66
|
+
class: "cn-settings-form__file",
|
|
67
|
+
"data-testid": `cn-settings-form-file-${field.key}`
|
|
68
|
+
}, [
|
|
69
|
+
createElementVNode("span", {
|
|
70
|
+
id: $options.inputId(field),
|
|
71
|
+
class: normalizeClass(["cn-settings-form__control cn-settings-form__control--file", { 'cn-settings-form__control--placeholder': !$data.form[field.key] }])
|
|
72
|
+
}, toDisplayString($data.form[field.key] ? $options.fileName($data.form[field.key]) : field.placeholder), 11 /* TEXT, CLASS, PROPS */, _hoisted_4),
|
|
73
|
+
(!field.readOnly)
|
|
74
|
+
? (openBlock(), createBlock(_component_NcButton, {
|
|
75
|
+
key: 0,
|
|
76
|
+
variant: "secondary",
|
|
77
|
+
"aria-describedby": $options.inputId(field),
|
|
78
|
+
"data-testid": `cn-settings-form-choose-${field.key}`,
|
|
79
|
+
onClick: $event => ($options.chooseFile(field))
|
|
80
|
+
}, {
|
|
81
|
+
default: withCtx(() => [
|
|
82
|
+
createTextVNode(toDisplayString(field.chooseLabel), 1 /* TEXT */)
|
|
83
|
+
]),
|
|
84
|
+
_: 2 /* DYNAMIC */
|
|
85
|
+
}, 1032 /* PROPS, DYNAMIC_SLOTS */, ["aria-describedby", "data-testid", "onClick"]))
|
|
86
|
+
: createCommentVNode("v-if", true),
|
|
87
|
+
(!field.readOnly && $data.form[field.key])
|
|
88
|
+
? (openBlock(), createBlock(_component_NcButton, {
|
|
89
|
+
key: 1,
|
|
90
|
+
variant: "tertiary",
|
|
91
|
+
"data-testid": `cn-settings-form-clear-${field.key}`,
|
|
92
|
+
onClick: $event => ($data.form[field.key] = '')
|
|
93
|
+
}, {
|
|
94
|
+
default: withCtx(() => [
|
|
95
|
+
createTextVNode(toDisplayString(field.clearLabel), 1 /* TEXT */)
|
|
96
|
+
]),
|
|
97
|
+
_: 2 /* DYNAMIC */
|
|
98
|
+
}, 1032 /* PROPS, DYNAMIC_SLOTS */, ["data-testid", "onClick"]))
|
|
99
|
+
: createCommentVNode("v-if", true)
|
|
100
|
+
], 8 /* PROPS */, _hoisted_3))
|
|
101
|
+
: (field.type === 'select')
|
|
102
|
+
? withDirectives((openBlock(), createElementBlock("select", {
|
|
103
|
+
key: 1,
|
|
104
|
+
id: $options.inputId(field),
|
|
105
|
+
"onUpdate:modelValue": $event => (($data.form[field.key]) = $event),
|
|
106
|
+
class: "cn-settings-form__control",
|
|
107
|
+
disabled: field.readOnly
|
|
108
|
+
}, [
|
|
109
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(field.options, (option) => {
|
|
110
|
+
return (openBlock(), createElementBlock("option", {
|
|
111
|
+
key: String(option.value),
|
|
112
|
+
value: option.value
|
|
113
|
+
}, toDisplayString(option.label), 9 /* TEXT, PROPS */, _hoisted_6))
|
|
114
|
+
}), 128 /* KEYED_FRAGMENT */))
|
|
115
|
+
], 8 /* PROPS */, _hoisted_5)), [
|
|
116
|
+
[vModelSelect, $data.form[field.key]]
|
|
117
|
+
])
|
|
118
|
+
: (field.type === 'textarea')
|
|
119
|
+
? withDirectives((openBlock(), createElementBlock("textarea", {
|
|
120
|
+
key: 2,
|
|
121
|
+
id: $options.inputId(field),
|
|
122
|
+
"onUpdate:modelValue": $event => (($data.form[field.key]) = $event),
|
|
123
|
+
class: "cn-settings-form__control cn-settings-form__control--area",
|
|
124
|
+
rows: field.rows,
|
|
125
|
+
placeholder: field.placeholder,
|
|
126
|
+
readonly: field.readOnly
|
|
127
|
+
}, null, 8 /* PROPS */, _hoisted_7)), [
|
|
128
|
+
[vModelText, $data.form[field.key]]
|
|
129
|
+
])
|
|
130
|
+
: withDirectives((openBlock(), createElementBlock("input", {
|
|
131
|
+
key: 3,
|
|
132
|
+
id: $options.inputId(field),
|
|
133
|
+
"onUpdate:modelValue": $event => (($data.form[field.key]) = $event),
|
|
134
|
+
class: "cn-settings-form__control",
|
|
135
|
+
type: field.type === 'number' ? 'number' : 'text',
|
|
136
|
+
min: field.min,
|
|
137
|
+
max: field.max,
|
|
138
|
+
placeholder: field.placeholder,
|
|
139
|
+
readonly: field.readOnly,
|
|
140
|
+
"aria-invalid": $data.errors[field.key] ? 'true' : null
|
|
141
|
+
}, null, 8 /* PROPS */, _hoisted_8)), [
|
|
142
|
+
[vModelDynamic, $data.form[field.key]]
|
|
143
|
+
])
|
|
144
|
+
], 64 /* STABLE_FRAGMENT */)),
|
|
145
|
+
($data.errors[field.key])
|
|
146
|
+
? (openBlock(), createElementBlock("p", _hoisted_9, toDisplayString($data.errors[field.key]), 1 /* TEXT */))
|
|
147
|
+
: (field.hint)
|
|
148
|
+
? (openBlock(), createElementBlock("p", _hoisted_10, toDisplayString(field.hint), 1 /* TEXT */))
|
|
149
|
+
: createCommentVNode("v-if", true)
|
|
150
|
+
], 10 /* CLASS, PROPS */, _hoisted_1))
|
|
151
|
+
}), 128 /* KEYED_FRAGMENT */))
|
|
152
|
+
], 2 /* CLASS */),
|
|
153
|
+
($data.notice)
|
|
154
|
+
? (openBlock(), createBlock(_component_NcNoteCard, {
|
|
155
|
+
key: 0,
|
|
156
|
+
type: $data.noticeType,
|
|
157
|
+
"data-testid": "cn-settings-form-notice"
|
|
158
|
+
}, {
|
|
159
|
+
default: withCtx(() => [
|
|
160
|
+
createTextVNode(toDisplayString($data.notice), 1 /* TEXT */)
|
|
161
|
+
]),
|
|
162
|
+
_: 1 /* STABLE */
|
|
163
|
+
}, 8 /* PROPS */, ["type"]))
|
|
164
|
+
: createCommentVNode("v-if", true),
|
|
165
|
+
(!$options.readOnly)
|
|
166
|
+
? (openBlock(), createElementBlock("div", _hoisted_11, [
|
|
167
|
+
createVNode(_component_NcButton, {
|
|
168
|
+
variant: "primary",
|
|
169
|
+
type: "submit",
|
|
170
|
+
disabled: $data.saving,
|
|
171
|
+
"data-testid": "cn-settings-form-save"
|
|
172
|
+
}, {
|
|
173
|
+
default: withCtx(() => [
|
|
174
|
+
createTextVNode(toDisplayString($options.saveLabel), 1 /* TEXT */)
|
|
175
|
+
]),
|
|
176
|
+
_: 1 /* STABLE */
|
|
177
|
+
}, 8 /* PROPS */, ["disabled"])
|
|
178
|
+
]))
|
|
179
|
+
: createCommentVNode("v-if", true)
|
|
180
|
+
], 32 /* NEED_HYDRATION */))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export { render };
|
|
184
|
+
//# sourceMappingURL=CnSettingsFormWidget.vue3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnSettingsFormWidget.vue3.js","sources":["../../../../src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue?vue&type=template&id=731c450f&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<form class=\"cn-settings-form\"\n\t\tdata-testid=\"cn-settings-form\"\n\t\tnovalidate\n\t\t@submit.prevent=\"save\">\n\t\t<div class=\"cn-settings-form__grid\" :class=\"`cn-settings-form__grid--${columns}`\">\n\t\t\t<div\n\t\t\t\tv-for=\"field in fields\"\n\t\t\t\t:key=\"field.key\"\n\t\t\t\tclass=\"cn-settings-form__field\"\n\t\t\t\t:class=\"{ 'cn-settings-form__field--wide': field.wide }\"\n\t\t\t\t:data-testid=\"`cn-settings-form-field-${field.key}`\">\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\tv-if=\"field.type === 'switch'\"\n\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\ttype=\"switch\"\n\t\t\t\t\t:disabled=\"field.readOnly\">\n\t\t\t\t\t{{ field.label }}\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t<template v-else>\n\t\t\t\t\t<label class=\"cn-settings-form__label\" :for=\"inputId(field)\">{{ field.label }}</label>\n\t\t\t\t\t<div v-if=\"field.type === 'file'\" class=\"cn-settings-form__file\" :data-testid=\"`cn-settings-form-file-${field.key}`\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\t\tclass=\"cn-settings-form__control cn-settings-form__control--file\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-settings-form__control--placeholder': !form[field.key] }\">\n\t\t\t\t\t\t\t{{ form[field.key] ? fileName(form[field.key]) : field.placeholder }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-if=\"!field.readOnly\"\n\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t:aria-describedby=\"inputId(field)\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-settings-form-choose-${field.key}`\"\n\t\t\t\t\t\t\t@click=\"chooseFile(field)\">\n\t\t\t\t\t\t\t{{ field.chooseLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-if=\"!field.readOnly && form[field.key]\"\n\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-settings-form-clear-${field.key}`\"\n\t\t\t\t\t\t\t@click=\"form[field.key] = ''\">\n\t\t\t\t\t\t\t{{ field.clearLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t\t<select\n\t\t\t\t\t\tv-else-if=\"field.type === 'select'\"\n\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\t\tclass=\"cn-settings-form__control\"\n\t\t\t\t\t\t:disabled=\"field.readOnly\">\n\t\t\t\t\t\t<option v-for=\"option in field.options\" :key=\"String(option.value)\" :value=\"option.value\">\n\t\t\t\t\t\t\t{{ option.label }}\n\t\t\t\t\t\t</option>\n\t\t\t\t\t</select>\n\t\t\t\t\t<textarea\n\t\t\t\t\t\tv-else-if=\"field.type === 'textarea'\"\n\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\t\tclass=\"cn-settings-form__control cn-settings-form__control--area\"\n\t\t\t\t\t\t:rows=\"field.rows\"\n\t\t\t\t\t\t:placeholder=\"field.placeholder\"\n\t\t\t\t\t\t:readonly=\"field.readOnly\" />\n\t\t\t\t\t<input\n\t\t\t\t\t\tv-else\n\t\t\t\t\t\t:id=\"inputId(field)\"\n\t\t\t\t\t\tv-model=\"form[field.key]\"\n\t\t\t\t\t\tclass=\"cn-settings-form__control\"\n\t\t\t\t\t\t:type=\"field.type === 'number' ? 'number' : 'text'\"\n\t\t\t\t\t\t:min=\"field.min\"\n\t\t\t\t\t\t:max=\"field.max\"\n\t\t\t\t\t\t:placeholder=\"field.placeholder\"\n\t\t\t\t\t\t:readonly=\"field.readOnly\"\n\t\t\t\t\t\t:aria-invalid=\"errors[field.key] ? 'true' : null\">\n\t\t\t\t</template>\n\t\t\t\t<p v-if=\"errors[field.key]\" class=\"cn-settings-form__error\" role=\"alert\">\n\t\t\t\t\t{{ errors[field.key] }}\n\t\t\t\t</p>\n\t\t\t\t<p v-else-if=\"field.hint\" class=\"cn-settings-form__hint\">\n\t\t\t\t\t{{ field.hint }}\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<NcNoteCard v-if=\"notice\" :type=\"noticeType\" data-testid=\"cn-settings-form-notice\">\n\t\t\t{{ notice }}\n\t\t</NcNoteCard>\n\n\t\t<div v-if=\"!readOnly\" class=\"cn-settings-form__actions\">\n\t\t\t<NcButton\n\t\t\t\tvariant=\"primary\"\n\t\t\t\ttype=\"submit\"\n\t\t\t\t:disabled=\"saving\"\n\t\t\t\tdata-testid=\"cn-settings-form-save\">\n\t\t\t\t{{ saveLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\t</form>\n</template>\n\n<script>\nimport { FilePickerClosed, getFilePickerBuilder } from '@nextcloud/dialogs'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcCheckboxRadioSwitch, NcNoteCard } from '@nextcloud/vue'\nimport { changedFields, objectUrlOf, patchObject } from '../../utils/objectWrite.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\n\n// The native file-picker modal ships its chrome styles here; without this the\n// spawned picker renders unstyled. Rules are scoped to the picker/dialog classes.\nimport '@nextcloud/dialogs/style.css'\n\nconst TYPES = ['text', 'textarea', 'select', 'switch', 'number', 'file']\n\nlet uid = 0\n\n/**\n * CnSettingsFormWidget — the `settings-form` widget: fields of the CURRENT\n * object as a one- or two-column grid of framed inputs with light labels and\n * hints, saved by ONE PATCH of the fields that changed since the last save.\n *\n * \"Last save\" is the baseline this widget keeps: a save moves it forward, so\n * editing a field back to the value the page loaded with, after a save, still\n * counts as a change (comparing against the loaded object skipped that PATCH).\n *\n * @spec openspec/changes/abstract-settings-form/specs/settings-form/spec.md\n */\nexport default {\n\tname: 'CnSettingsFormWidget',\n\n\tcomponents: { NcButton, NcCheckboxRadioSwitch, NcNoteCard },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `fields` (required) — `[{ key, label, type?: 'text'|'textarea'|'select'|'switch'|'number'|'file', picker?: { title?, mimeTypes?, startAt?, valueTemplate? }, chooseLabel?, clearLabel?, hint?, placeholder?, options?: [{ value, label }], required?, pattern?, patternMessage?, min?, max?, rows?, wide?, readOnly? }]`; `key` is a top-level field of the object.\n\t\t * - `columns` — 1 or 2 (default 2).\n\t\t * - `saveLabel`, `savedText`, `readOnly`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The current detail-page object. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current object id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['saved'],\n\n\tdata() {\n\t\tuid += 1\n\t\treturn {\n\t\t\tuid,\n\t\t\tform: {},\n\t\t\tbaseline: {},\n\t\t\terrors: {},\n\t\t\tsaving: false,\n\t\t\tnotice: '',\n\t\t\tnoticeType: 'success',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The configured fields, normalised and translated.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tfields() {\n\t\t\tconst list = Array.isArray(this.content.fields) ? this.content.fields : []\n\t\t\treturn list\n\t\t\t\t.filter((field) => field && field.key)\n\t\t\t\t.map((field) => ({\n\t\t\t\t\t...field,\n\t\t\t\t\ttype: TYPES.includes(field.type) ? field.type : 'text',\n\t\t\t\t\tlabel: this.tr(field.label || field.key),\n\t\t\t\t\thint: this.tr(field.hint || ''),\n\t\t\t\t\tplaceholder: this.tr(field.placeholder || ''),\n\t\t\t\t\tchooseLabel: this.tr(field.chooseLabel || 'Choose a file'),\n\t\t\t\t\tclearLabel: this.tr(field.clearLabel || 'Remove'),\n\t\t\t\t\trows: field.rows || 3,\n\t\t\t\t\twide: field.wide === true || field.type === 'textarea',\n\t\t\t\t\treadOnly: this.readOnly || field.readOnly === true,\n\t\t\t\t\toptions: (Array.isArray(field.options) ? field.options : []).map((option) => (\n\t\t\t\t\t\ttypeof option === 'object' && option !== null\n\t\t\t\t\t\t\t? { value: option.value, label: this.tr(String(option.label ?? option.value)) }\n\t\t\t\t\t\t\t: { value: option, label: this.tr(String(option)) }\n\t\t\t\t\t)),\n\t\t\t\t}))\n\t\t},\n\n\t\tcolumns() {\n\t\t\treturn this.content.columns === 1 ? 1 : 2\n\t\t},\n\n\t\treadOnly() {\n\t\t\treturn this.content.readOnly === true\n\t\t},\n\n\t\tsaveLabel() {\n\t\t\treturn this.tr(this.content.saveLabel || 'Save')\n\t\t},\n\n\t\t/** The object URL the form is saved to, or '' when it is not known. */\n\t\tobjectUrl() {\n\t\t\treturn objectUrlOf(this.register, this.schema, this.objectId || (this.objectData && this.objectData.id))\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectData() {\n\t\t\tthis.adopt()\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.adopt(true)\n\t},\n\n\tmethods: {\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\tinputId(field) {\n\t\t\treturn `cn-settings-form-${this.uid}-${field.key}`\n\t\t},\n\n\t\t/**\n\t\t * The name shown for a stored file value: its last path segment.\n\t\t *\n\t\t * @param {string} value The stored value.\n\t\t * @return {string}\n\t\t */\n\t\tfileName(value) {\n\t\t\tconst text = String(value)\n\t\t\treturn text.split('/').filter(Boolean).pop() || text\n\t\t},\n\n\t\t/**\n\t\t * Open the Nextcloud file picker and take the chosen file as the value.\n\t\t * `field.picker` = `{ title?, mimeTypes?, startAt?, valueTemplate? }`; the\n\t\t * template's `{path}`, `{fileid}` and `{name}` are filled from the chosen\n\t\t * node (default `{path}`).\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-settings-form-file/specs/settings-form/spec.md#requirement-a-file-field-takes-a-file-from-the-nextcloud-picker\n\t\t */\n\t\tasync chooseFile(field) {\n\t\t\tconst picker = field.picker || {}\n\t\t\tlet builder = getFilePickerBuilder(this.tr(picker.title || field.label))\n\t\t\t\t.setMultiSelect(false)\n\t\t\t\t.allowDirectories(false)\n\t\t\t\t.startAt(picker.startAt || '/')\n\t\t\t\t.addButton({ label: t('nextcloud-vue', 'Choose'), type: 'primary', callback: () => {} })\n\t\t\tif (Array.isArray(picker.mimeTypes) && picker.mimeTypes.length > 0) {\n\t\t\t\tbuilder = builder.setMimeTypeFilter(picker.mimeTypes)\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst nodes = await builder.build().pickNodes()\n\t\t\t\tconst node = Array.isArray(nodes) ? nodes[0] : nodes\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.form[field.key] = String(picker.valueTemplate || '{path}').replace(/\\{(path|fileid|name)\\}/g, (match, part) => {\n\t\t\t\t\tconst value = part === 'name' ? (node.basename || this.fileName(node.path)) : node[part]\n\t\t\t\t\treturn value === undefined || value === null ? '' : String(value)\n\t\t\t\t})\n\t\t\t} catch (e) {\n\t\t\t\tif (e instanceof FilePickerClosed) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.noticeType = 'error'\n\t\t\t\tthis.notice = this.tr('The file picker could not be opened.')\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The stored value of a field as the input holds it.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {unknown}\n\t\t */\n\t\tstored(field) {\n\t\t\tconst value = readPath(this.objectData || {}, field.key)\n\t\t\tif (field.type === 'switch') {\n\t\t\t\treturn value === true\n\t\t\t}\n\t\t\treturn value === undefined || value === null ? '' : value\n\t\t},\n\n\t\t/**\n\t\t * Take the object's values as the new baseline; a field the person has\n\t\t * not touched follows it, a touched one keeps the typed value.\n\t\t *\n\t\t * @param {boolean} [first] Whether this is the first read.\n\t\t */\n\t\tadopt(first = false) {\n\t\t\tconst baseline = {}\n\t\t\tconst form = {}\n\t\t\tfor (const field of this.fields) {\n\t\t\t\tbaseline[field.key] = this.stored(field)\n\t\t\t\tconst untouched = first || JSON.stringify(this.form[field.key]) === JSON.stringify(this.baseline[field.key])\n\t\t\t\tform[field.key] = untouched ? baseline[field.key] : this.form[field.key]\n\t\t\t}\n\t\t\tthis.baseline = baseline\n\t\t\tthis.form = form\n\t\t},\n\n\t\t/**\n\t\t * The value to send for a field: numbers as numbers, an empty value as null.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {unknown}\n\t\t */\n\t\toutgoing(field) {\n\t\t\tconst value = this.form[field.key]\n\t\t\tif (field.type === 'number') {\n\t\t\t\treturn value === '' || value === null || value === undefined ? null : Number(value)\n\t\t\t}\n\t\t\tif (field.type === 'switch') {\n\t\t\t\treturn value === true\n\t\t\t}\n\t\t\tif (typeof value === 'string') {\n\t\t\t\treturn value.trim() === '' ? null : value.trim()\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\n\t\t/**\n\t\t * Check required, number range and pattern before any write.\n\t\t *\n\t\t * @return {object} Error text by field key.\n\t\t */\n\t\tvalidate() {\n\t\t\tconst errors = {}\n\t\t\tfor (const field of this.fields) {\n\t\t\t\tif (field.type === 'switch' || field.readOnly) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst value = this.outgoing(field)\n\t\t\t\tconst empty = value === null || value === undefined || value === ''\n\t\t\t\tif (empty) {\n\t\t\t\t\tif (field.required) {\n\t\t\t\t\t\terrors[field.key] = this.tr('Fill this in.')\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (field.type === 'number') {\n\t\t\t\t\tif (Number.isNaN(value) || (field.min !== undefined && value < field.min) || (field.max !== undefined && value > field.max)) {\n\t\t\t\t\t\terrors[field.key] = this.tr('This number is not valid.')\n\t\t\t\t\t}\n\t\t\t\t} else if (field.pattern && !new RegExp(field.pattern).test(String(value))) {\n\t\t\t\t\terrors[field.key] = this.tr(field.patternMessage || 'This value is not valid.')\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn errors\n\t\t},\n\n\t\t/**\n\t\t * Save: PATCH the fields that differ from the last saved values.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-settings-form/specs/settings-form/spec.md#requirement-a-settings-form-saves-only-what-changed\n\t\t */\n\t\tasync save() {\n\t\t\tthis.errors = this.validate()\n\t\t\tthis.notice = ''\n\t\t\tif (Object.keys(this.errors).length > 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.objectUrl) {\n\t\t\t\tthis.noticeType = 'error'\n\t\t\t\tthis.notice = this.tr('This form cannot be saved here.')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = {}\n\t\t\tconst compare = {}\n\t\t\tfor (const field of this.fields) {\n\t\t\t\tif (!field.readOnly) {\n\t\t\t\t\tnext[field.key] = this.outgoing(field)\n\t\t\t\t\tcompare[field.key] = this.baseline[field.key] === '' ? null : this.baseline[field.key]\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst patch = changedFields(compare, next)\n\t\t\tthis.saving = true\n\t\t\ttry {\n\t\t\t\tif (Object.keys(patch).length > 0) {\n\t\t\t\t\tconst data = await patchObject(this.objectUrl, patch)\n\t\t\t\t\tconst baseline = { ...this.baseline }\n\t\t\t\t\tconst form = { ...this.form }\n\t\t\t\t\tfor (const key of Object.keys(patch)) {\n\t\t\t\t\t\tconst field = this.fields.find((candidate) => candidate.key === key)\n\t\t\t\t\t\tconst written = data[key] !== undefined ? data[key] : patch[key]\n\t\t\t\t\t\tbaseline[key] = field.type === 'switch' ? written === true : (written === null ? '' : written)\n\t\t\t\t\t\tform[key] = baseline[key]\n\t\t\t\t\t}\n\t\t\t\t\tthis.baseline = baseline\n\t\t\t\t\tthis.form = form\n\t\t\t\t}\n\t\t\t\tthis.noticeType = 'success'\n\t\t\t\tthis.notice = this.tr(this.content.savedText || 'Saved.')\n\t\t\t\t/**\n\t\t\t\t * @event saved The changed fields were written.\n\t\t\t\t * @type {{patch: object}}\n\t\t\t\t */\n\t\t\t\tthis.$emit('saved', { patch })\n\t\t\t} catch (e) {\n\t\t\t\tthis.noticeType = 'error'\n\t\t\t\tthis.notice = serverErrorMessage(e)\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-settings-form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 14px;\n\tmin-width: 0;\n}\n\n.cn-settings-form__grid {\n\tdisplay: grid;\n\tgap: 14px 16px;\n\tgrid-template-columns: minmax(0, 1fr);\n}\n\n.cn-settings-form__grid--2 {\n\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n}\n\n.cn-settings-form__field {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\tmin-width: 0;\n}\n\n.cn-settings-form__field--wide {\n\tgrid-column: 1 / -1;\n}\n\n.cn-settings-form__label {\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-settings-form__control {\n\twidth: 100%;\n\tbox-sizing: border-box;\n\theight: var(--cn-board-control-height, 40px);\n\tpadding: 0 12px;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: var(--cn-board-control-radius, 8px);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tfont: inherit;\n}\n\n.cn-settings-form__file {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.cn-settings-form__control--file {\n\tdisplay: inline-flex;\n\talign-items: center;\n\twidth: auto;\n\tmin-width: 120px;\n\tflex: 0 1 auto;\n\toverflow-wrap: anywhere;\n}\n\n.cn-settings-form__control--placeholder {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-settings-form__control--area {\n\theight: auto;\n\tpadding: 10px 12px;\n\tresize: vertical;\n}\n\n.cn-settings-form__control:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 1px;\n}\n\n.cn-settings-form__hint,\n.cn-settings-form__error {\n\tmargin: 0;\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-settings-form__error {\n\tcolor: var(--color-error-text);\n}\n\n.cn-settings-form__actions {\n\tdisplay: flex;\n\tjustify-content: flex-start;\n}\n\n@media (max-width: 640px) {\n\t.cn-settings-form__grid--2 {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_Fragment","_renderList","_createBlock","_createTextVNode","_toDisplayString","_createElementVNode","_openBlock"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import __sfc_main from './CnSettingsFormWidget.vue.js';
|
|
2
|
+
import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
6
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
7
|
+
*
|
|
8
|
+
* CnSettingsFormWidget — self-registers the `settings-form` widget type (framed
|
|
9
|
+
* inputs for fields of the current object, saved by PATCH of the changed
|
|
10
|
+
* fields) into the shared dashboardWidgetRegistry at module load.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
registerDashboardWidget('settings-form', {
|
|
15
|
+
renderer: __sfc_main,
|
|
16
|
+
form: null,
|
|
17
|
+
defaultContent: {
|
|
18
|
+
fields: [],
|
|
19
|
+
columns: 2,
|
|
20
|
+
},
|
|
21
|
+
displayName: 'Settings form',
|
|
22
|
+
icon: 'FormTextbox',
|
|
23
|
+
surfaces: ['detail-page'],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export { __sfc_main as CnSettingsFormWidget, __sfc_main as default };
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/components/CnSettingsFormWidget/index.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * CnSettingsFormWidget — self-registers the `settings-form` widget type (framed\n * inputs for fields of the current object, saved by PATCH of the changed\n * fields) into the shared dashboardWidgetRegistry at module load.\n */\n\nimport CnSettingsFormWidget from './CnSettingsFormWidget.vue'\nimport { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'\n\nregisterDashboardWidget('settings-form', {\n\trenderer: CnSettingsFormWidget,\n\tform: null,\n\tdefaultContent: {\n\t\tfields: [],\n\t\tcolumns: 2,\n\t},\n\tdisplayName: 'Settings form',\n\ticon: 'FormTextbox',\n\tsurfaces: ['detail-page'],\n})\n\nexport { CnSettingsFormWidget }\nexport default CnSettingsFormWidget\n"],"names":["CnSettingsFormWidget"],"mappings":";;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA;AACA,uBAAuB,CAAC,eAAe,EAAE;AACzC,CAAC,QAAQ,EAAEA,UAAoB;AAC/B,CAAC,IAAI,EAAE,IAAI;AACX,CAAC,cAAc,EAAE;AACjB,EAAE,MAAM,EAAE,EAAE;AACZ,EAAE,OAAO,EAAE,CAAC;AACZ,EAAE;AACF,CAAC,WAAW,EAAE,eAAe;AAC7B,CAAC,IAAI,EAAE,aAAa;AACpB,CAAC,QAAQ,EAAE,CAAC,aAAa,CAAC;AAC1B,CAAC;;;;"}
|
|
@@ -12,6 +12,7 @@ import widgetLink from '../../mixins/widgetLink.js';
|
|
|
12
12
|
import { useObjectStore } from '../../store/useObjectStore.js';
|
|
13
13
|
import { resolveObjectOpType } from '../../utils/actionsDispatcher.js';
|
|
14
14
|
import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js';
|
|
15
|
+
import { needsListEndpoint, fetchListTotal } from '../../utils/fetchFilterCounts.js';
|
|
15
16
|
import { unwrapAppConfig, formatMetricValue } from '../../utils/formatMetric.js';
|
|
16
17
|
import { dropOptionalUnresolved, resolveFilterTokens } from '../../utils/resolveFilterTokens.js';
|
|
17
18
|
import { STATUS_TEXT_COLORS } from '../../utils/statusColors.js';
|
|
@@ -1586,8 +1587,7 @@ var script = {
|
|
|
1586
1587
|
// drop any optional `@workspace.<key>?` / `@config.<key>?` that stayed
|
|
1587
1588
|
// unresolved so an unset value omits the filter (show all) instead of
|
|
1588
1589
|
// sending a literal token.
|
|
1589
|
-
|
|
1590
|
-
filter = dropOptionalUnresolved(resolveFilterTokens(filter, ctx));
|
|
1590
|
+
filter = this.resolveSourceFilter(filter);
|
|
1591
1591
|
for (const [k, v] of Object.entries(filter)) {
|
|
1592
1592
|
if (v && typeof v === 'object') {
|
|
1593
1593
|
for (const [op, ov] of Object.entries(v)) {
|
|
@@ -1599,6 +1599,21 @@ var script = {
|
|
|
1599
1599
|
}
|
|
1600
1600
|
},
|
|
1601
1601
|
|
|
1602
|
+
/**
|
|
1603
|
+
* A source filter with its @-tokens resolved against the object, page
|
|
1604
|
+
* and config contexts, and unresolved optional clauses dropped.
|
|
1605
|
+
*
|
|
1606
|
+
* @param {object} filter The raw filter map.
|
|
1607
|
+
* @return {object} The resolved filter.
|
|
1608
|
+
*/
|
|
1609
|
+
resolveSourceFilter(filter) {
|
|
1610
|
+
if (!filter || typeof filter !== 'object') {
|
|
1611
|
+
return {}
|
|
1612
|
+
}
|
|
1613
|
+
const ctx = { ...(this.objectCtx || {}), workspace: this.pageCtx, config: this.configCtx };
|
|
1614
|
+
return dropOptionalUnresolved(resolveFilterTokens(filter, ctx))
|
|
1615
|
+
},
|
|
1616
|
+
|
|
1602
1617
|
/**
|
|
1603
1618
|
* Fetch one scalar from the OpenRegister `/value` aggregation endpoint.
|
|
1604
1619
|
*
|
|
@@ -1608,9 +1623,19 @@ var script = {
|
|
|
1608
1623
|
* @param {string} metric The aggregation metric.
|
|
1609
1624
|
* @param {?string} field The numeric field (non-count metrics).
|
|
1610
1625
|
* @param {object} filter The filter map.
|
|
1626
|
+
* @param {string} [via] `"list"` counts through the list endpoint's total.
|
|
1611
1627
|
* @return {Promise<number|null>} The aggregated value.
|
|
1612
1628
|
*/
|
|
1613
|
-
async fetchAggregate(axios, generateUrl, s, metric, field, filter) {
|
|
1629
|
+
async fetchAggregate(axios, generateUrl, s, metric, field, filter, via) {
|
|
1630
|
+
// A count the aggregation endpoint cannot express (a filter holding a
|
|
1631
|
+
// list of values, a test for no value (`null` or `{ isnull: true }`),
|
|
1632
|
+
// or `via: "list"`) reads the list endpoint's total instead, because
|
|
1633
|
+
// the aggregation endpoint has no null operator.
|
|
1634
|
+
const resolved = this.resolveSourceFilter(filter);
|
|
1635
|
+
const listed = Object.values(resolved).some((v) => Array.isArray(v) || (v && typeof v === 'object' && Object.values(v).some(Array.isArray)));
|
|
1636
|
+
if ((!metric || metric === 'count') && (via === 'list' || listed || needsListEndpoint(resolved))) {
|
|
1637
|
+
return fetchListTotal(s.register, s.schema, resolved, {})
|
|
1638
|
+
}
|
|
1614
1639
|
const url = generateUrl(
|
|
1615
1640
|
'/apps/openregister/api/objects/aggregations/{register}/{schema}/value',
|
|
1616
1641
|
{ register: s.register, schema: s.schema },
|
|
@@ -1667,9 +1692,15 @@ var script = {
|
|
|
1667
1692
|
if (s.kind === 'endpoint') {
|
|
1668
1693
|
this.value = await this.fetchEndpoint(s, refresh);
|
|
1669
1694
|
} else if (s.kind === 'ratio') {
|
|
1670
|
-
|
|
1671
|
-
|
|
1695
|
+
// Each part may name its own metric, field and `via`, so "answered
|
|
1696
|
+
// in time" over "answered" is two counts, and a share of a sum is
|
|
1697
|
+
// two sums.
|
|
1698
|
+
const part = (p) => this.fetchAggregate(axios, generateUrl, s, (p && p.metric) || s.metric, (p && p.field) || s.field, (p && p.filter) || {}, (p && p.via) || s.via);
|
|
1699
|
+
const num = await part(s.numerator);
|
|
1700
|
+
const den = await part(s.denominator);
|
|
1672
1701
|
this.value = (den && Number(den) !== 0) ? (Number(num) / Number(den)) * 100 : null;
|
|
1702
|
+
} else if (s.kind === 'elapsed') {
|
|
1703
|
+
this.value = await this.fetchElapsed(axios, generateUrl, s);
|
|
1673
1704
|
} else if (s.kind === 'computed') {
|
|
1674
1705
|
// Fetch each named part, then evaluate the formula over them.
|
|
1675
1706
|
const { evalFormula } = await import('../../utils/evalFormula.js');
|
|
@@ -1682,7 +1713,7 @@ var script = {
|
|
|
1682
1713
|
} else if (s.kind === 'weighted') {
|
|
1683
1714
|
this.value = await this.fetchWeighted(axios, generateUrl, s);
|
|
1684
1715
|
} else {
|
|
1685
|
-
this.value = await this.fetchAggregate(axios, generateUrl, s, s.metric, s.field, s.filter || {});
|
|
1716
|
+
this.value = await this.fetchAggregate(axios, generateUrl, s, s.metric, s.field, s.filter || {}, s.via);
|
|
1686
1717
|
}
|
|
1687
1718
|
} catch (e) {
|
|
1688
1719
|
this.error = (e && e.message) || 'error';
|
|
@@ -1692,6 +1723,56 @@ var script = {
|
|
|
1692
1723
|
}
|
|
1693
1724
|
},
|
|
1694
1725
|
|
|
1726
|
+
/**
|
|
1727
|
+
* The average time between two date fields of the matching objects
|
|
1728
|
+
* (`{ from, to, unit?, filter?, limit? }`): an average answer time is
|
|
1729
|
+
* `occurredAt` to `resolvedAt`. Computed client-side over the fetched
|
|
1730
|
+
* objects (capped at `limit`, default 1000), because the aggregation
|
|
1731
|
+
* endpoint averages one numeric field, not a difference. Objects missing
|
|
1732
|
+
* either date, or with `to` before `from`, are left out; null when none
|
|
1733
|
+
* is left. `unit` is `days` (default), `hours` or `minutes`.
|
|
1734
|
+
*
|
|
1735
|
+
* @param {object} axios The axios instance.
|
|
1736
|
+
* @param {(url: string, params?: object) => string} generateUrl The router helper.
|
|
1737
|
+
* @param {object} s The elapsed source.
|
|
1738
|
+
* @return {Promise<number|null>} The average, in `unit`.
|
|
1739
|
+
* @spec openspec/changes/screens-ratio-stat/specs/ratio-stat/spec.md#requirement-a-stat-can-average-the-time-between-two-dates
|
|
1740
|
+
*/
|
|
1741
|
+
async fetchElapsed(axios, generateUrl, s) {
|
|
1742
|
+
if (!s.from || !s.to) {
|
|
1743
|
+
return null
|
|
1744
|
+
}
|
|
1745
|
+
const url = generateUrl(
|
|
1746
|
+
'/apps/openregister/api/objects/{register}/{schema}',
|
|
1747
|
+
{ register: s.register, schema: s.schema },
|
|
1748
|
+
);
|
|
1749
|
+
const params = { _limit: s.limit || 1000 };
|
|
1750
|
+
const filter = this.resolveSourceFilter(s.filter || {});
|
|
1751
|
+
for (const [k, v] of Object.entries(filter)) {
|
|
1752
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
1753
|
+
for (const [op, operand] of Object.entries(v)) {
|
|
1754
|
+
params[`${k}[${op}]`] = operand;
|
|
1755
|
+
}
|
|
1756
|
+
} else if (v !== '' && v !== null && v !== undefined) {
|
|
1757
|
+
params[k] = v;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
const res = await axios.get(url, { params });
|
|
1761
|
+
const rows = (res && res.data && res.data.results) || [];
|
|
1762
|
+
const per = { minutes: 60000, hours: 3600000, days: 86400000 }[s.unit] || 86400000;
|
|
1763
|
+
let sum = 0;
|
|
1764
|
+
let n = 0;
|
|
1765
|
+
for (const r of rows) {
|
|
1766
|
+
const from = Date.parse(r && r[s.from]);
|
|
1767
|
+
const to = Date.parse(r && r[s.to]);
|
|
1768
|
+
if (Number.isFinite(from) && Number.isFinite(to) && to >= from) {
|
|
1769
|
+
sum += (to - from) / per;
|
|
1770
|
+
n += 1;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
return n > 0 ? sum / n : null
|
|
1774
|
+
},
|
|
1775
|
+
|
|
1695
1776
|
/**
|
|
1696
1777
|
* Compute a weighted sum `Σ (field × weightField) ÷ divisor` client-side
|
|
1697
1778
|
* (no OpenRegister expression-aggregation primitive yet). Pulls the
|