glib-web 4.3.0 → 4.4.0
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 +5 -4
- package/action.js +4 -0
- package/actions/components/replace.js +4 -7
- package/actions/components/replaceChildren.js +1 -0
- package/actions/components/set.js +1 -5
- package/actions/dialogs/close.js +2 -0
- package/actions/fields/getDynamicGroupEntryValues.js +23 -0
- package/actions/fields/reset.js +1 -1
- package/actions/logics/run.js +35 -0
- package/actions/logics/set.js +12 -1
- package/app.vue +9 -0
- package/components/composable/file.js +15 -2
- package/components/fields/_dynamicGroupEntry.vue +6 -0
- package/components/fields/_patternText.vue +1 -4
- package/components/fields/_select.vue +1 -4
- package/components/fields/dynamicGroup.vue +14 -2
- package/components/fields/phone/field.vue +3 -20
- package/components/fields/richText.vue +1 -1
- package/components/fields/text.vue +0 -4
- package/components/fields/textarea.vue +1 -4
- package/components/fields/timer.vue +1 -4
- package/components/mixins/generic.js +1 -2
- package/components/mixins/styles.js +1 -13
- package/components/panels/vertical.vue +11 -1
- package/components/validation.js +6 -6
- package/cypress/e2e/glib-web/autoValidate.cy.ts +23 -0
- package/cypress/e2e/glib-web/dialog.cy.ts +13 -1
- package/cypress/e2e/glib-web/display.cy.ts +28 -1
- package/cypress/e2e/glib-web/{reactivity.cy.ts → form.cy.ts} +4 -6
- package/cypress/e2e/glib-web/formDynamic.cy.ts +55 -0
- package/cypress/e2e/glib-web/multiupload.cy.ts +23 -0
- package/cypress/helper.ts +3 -3
- package/package.json +2 -2
- package/plugins/updatableComponent.js +37 -12
- package/store.js +14 -7
- package/utils/component.js +25 -12
package/README.md
CHANGED
|
@@ -46,18 +46,19 @@ view.chipGroup styleClasses: ['custom']
|
|
|
46
46
|
|
|
47
47
|
## Clean up to fix strange errors (e.g. tiny-emitter error) when running vite dev
|
|
48
48
|
|
|
49
|
-
- Stop vite server
|
|
49
|
+
- Stop vite server.
|
|
50
|
+
- Load the page without vite server, let the compilation finish.
|
|
51
|
+
- Then start vite server.
|
|
50
52
|
|
|
53
|
+
# Below doesn't work
|
|
51
54
|
- On your glib-web-npm's directory:
|
|
52
55
|
- `rm -rf node_modules`
|
|
53
56
|
- `yarn install`
|
|
54
57
|
|
|
55
58
|
- On your project's directory:
|
|
56
|
-
- `yarn unlink glib-web`
|
|
57
59
|
- `rm -rf node_modules`
|
|
58
|
-
- `bin/vite clobber`
|
|
59
|
-
- `yarn link glib-web`
|
|
60
60
|
- `yarn install`
|
|
61
|
+
- `bin/vite clobber`
|
|
61
62
|
- `bin/vite dev`
|
|
62
63
|
|
|
63
64
|
## Prepare for publishing
|
package/action.js
CHANGED
|
@@ -64,6 +64,7 @@ import ActionsFormsSubmit from "./actions/forms/submit";
|
|
|
64
64
|
import ActionsFieldsReset from "./actions/fields/reset";
|
|
65
65
|
import ActionsFieldsFocus from "./actions/fields/focus";
|
|
66
66
|
import ActionsFieldsBlur from "./actions/fields/blur";
|
|
67
|
+
import ActionsFieldsGetDynamicGroupEntryValues from "./actions/fields/getDynamicGroupEntryValues";
|
|
67
68
|
|
|
68
69
|
import ActionComponentsUpdate from "./actions/components/update";
|
|
69
70
|
import ActionComponentsFind from "./actions/components/find";
|
|
@@ -73,6 +74,7 @@ import ActionComponentsReplaceChildren from "./actions/components/replaceChildre
|
|
|
73
74
|
import ActionComponentsSet from "./actions/components/set";
|
|
74
75
|
|
|
75
76
|
import ActionLogicsSet from "./actions/logics/set";
|
|
77
|
+
import ActionLogicsRun from "./actions/logics/run";
|
|
76
78
|
|
|
77
79
|
import ActionListsAppend from "./actions/lists/append";
|
|
78
80
|
|
|
@@ -148,6 +150,7 @@ const actions = {
|
|
|
148
150
|
"fields/reset": ActionsFieldsReset,
|
|
149
151
|
"fields/focus": ActionsFieldsFocus,
|
|
150
152
|
"fields/blur": ActionsFieldsBlur,
|
|
153
|
+
"fields/getDynamicGroupEntryValues": ActionsFieldsGetDynamicGroupEntryValues,
|
|
151
154
|
|
|
152
155
|
"components/update": ActionComponentsUpdate,
|
|
153
156
|
"components/find": ActionComponentsFind,
|
|
@@ -157,6 +160,7 @@ const actions = {
|
|
|
157
160
|
"components/set": ActionComponentsSet,
|
|
158
161
|
|
|
159
162
|
"logics/set": ActionLogicsSet,
|
|
163
|
+
"logics/run": ActionLogicsRun,
|
|
160
164
|
|
|
161
165
|
"lists/append": ActionListsAppend,
|
|
162
166
|
|
|
@@ -4,16 +4,13 @@ import Action from "../../action";
|
|
|
4
4
|
export default class {
|
|
5
5
|
execute(spec, component) {
|
|
6
6
|
var target = component;
|
|
7
|
-
const targetId = spec.targetId
|
|
7
|
+
const targetId = spec.targetId;
|
|
8
8
|
if (targetId) {
|
|
9
9
|
target = GLib.component.findById(targetId);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} else {
|
|
16
|
-
console.warning("Component ID not found", targetId)
|
|
17
|
-
}
|
|
12
|
+
target.action_merge(spec.newView);
|
|
13
|
+
Action.execute(spec.onReplace, target);
|
|
14
|
+
|
|
18
15
|
}
|
|
19
16
|
}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// const setFileModel = (component, value) => {
|
|
2
|
-
// component.fieldModel = component.$sanitizeValue(component.$externalizeValue(value));
|
|
3
|
-
// };
|
|
4
|
-
|
|
5
1
|
|
|
6
2
|
export default class {
|
|
7
3
|
execute(spec, component) {
|
|
@@ -13,7 +9,7 @@ export default class {
|
|
|
13
9
|
|
|
14
10
|
Utils.type.ifObject(spec.data, (data) => {
|
|
15
11
|
targetComponent.action_merge(data);
|
|
16
|
-
|
|
12
|
+
|
|
17
13
|
});
|
|
18
14
|
|
|
19
15
|
GLib.action.execute(spec.onSet, targetComponent);
|
package/actions/dialogs/close.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export default class {
|
|
2
|
+
execute(spec, component) {
|
|
3
|
+
const dynamicGroupEntry = component.$closest("fields/internalDynamicGroupEntry");
|
|
4
|
+
if (dynamicGroupEntry) {
|
|
5
|
+
const values = dynamicGroupEntry.$values(spec.paramNameForFieldName || 'entry');
|
|
6
|
+
this.executeOnGet(spec, values)
|
|
7
|
+
} else {
|
|
8
|
+
console.error('Action invoked outside of dynamicGroup')
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
executeOnGet(spec, formData) {
|
|
13
|
+
Utils.type.ifObject(spec.onGet, onGet => {
|
|
14
|
+
const params = {
|
|
15
|
+
// Make sure to pass along the formData from the spec.
|
|
16
|
+
[spec.paramNameForFormData || 'formData']: Object.assign({}, spec.formData, formData)
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const data = Object.assign({}, onGet, params);
|
|
20
|
+
GLib.action.execute(data, this);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
package/actions/fields/reset.js
CHANGED
|
@@ -2,7 +2,7 @@ import { nextTick } from "vue";
|
|
|
2
2
|
|
|
3
3
|
export default class {
|
|
4
4
|
execute(properties, component) {
|
|
5
|
-
const target = GLib.component.findById(properties.targetId);
|
|
5
|
+
const target = GLib.component.findById(properties.targetId) || component;
|
|
6
6
|
target.action_resetValue();
|
|
7
7
|
|
|
8
8
|
nextTick(() => {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import jsonLogic from 'json-logic-js';
|
|
2
|
+
import { fieldModels } from "../../components/composable/conditional";
|
|
3
|
+
|
|
4
|
+
export default class {
|
|
5
|
+
execute(spec, component) {
|
|
6
|
+
let condition = spec.condition
|
|
7
|
+
|
|
8
|
+
const dynamicGroupEntry = component.$closest("fields/internalDynamicGroupEntry");
|
|
9
|
+
if (dynamicGroupEntry) {
|
|
10
|
+
condition = dynamicGroupEntry.$populateIndexes(condition);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const result = jsonLogic.apply(condition, Object.assign({}, fieldModels, spec.variables));
|
|
14
|
+
if (result) {
|
|
15
|
+
this.executeWithPassthroughParams(spec.onTrue, spec, component);
|
|
16
|
+
} else {
|
|
17
|
+
this.executeWithPassthroughParams(spec.onFalse, spec, component);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
executeWithPassthroughParams(actionSpec, originalSpec, component) {
|
|
22
|
+
if (!Utils.type.isObject(actionSpec)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const params = {
|
|
27
|
+
// Make sure to pass along the formData from the spec.
|
|
28
|
+
formData: originalSpec.formData
|
|
29
|
+
}
|
|
30
|
+
const data = Object.assign({}, actionSpec, params);
|
|
31
|
+
|
|
32
|
+
console.log("executeWithPassthroughParams1", data)
|
|
33
|
+
GLib.action.execute(data, component);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/actions/logics/set.js
CHANGED
|
@@ -45,7 +45,18 @@ export default class {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
if (Object.keys(data).length > 0) {
|
|
48
|
-
Array.isArray(targetComponent) ? targetComponent.forEach((comp) => comp.action_merge(data)) : targetComponent.action_merge(data);
|
|
48
|
+
// Array.isArray(targetComponent) ? targetComponent.forEach((comp) => comp.action_merge(data)) : targetComponent.action_merge(data);
|
|
49
|
+
|
|
50
|
+
const targetComponents = Array.isArray(targetComponent) ? targetComponent : [targetComponent]
|
|
51
|
+
targetComponents.forEach((comp) => {
|
|
52
|
+
comp.action_merge(data)
|
|
53
|
+
|
|
54
|
+
if (spec.cacheData) {
|
|
55
|
+
Utils.type.ifString(comp.viewId, (viewId) => {
|
|
56
|
+
GLib.component.registerData(viewId, data);
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
})
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
GLib.action.execute(spec.onSet, targetComponent);
|
package/app.vue
CHANGED
|
@@ -57,10 +57,23 @@ function useFileUtils() {
|
|
|
57
57
|
|
|
58
58
|
function useFilesState(files) {
|
|
59
59
|
const uploading = computed(() => {
|
|
60
|
-
|
|
60
|
+
if (Object.values(files.value).filter((v) => v.status != null).length > 0) {
|
|
61
|
+
Object.values(files.value).reduce((prev, curr) => {
|
|
62
|
+
if (curr.status == null) return prev;
|
|
63
|
+
return prev || curr.status == 'pending';
|
|
64
|
+
}, false);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return false;
|
|
61
68
|
});
|
|
62
69
|
const uploaded = computed(() => {
|
|
63
|
-
|
|
70
|
+
if (Object.values(files.value).filter((v) => v.status != null).length > 0) {
|
|
71
|
+
return Object.values(files.value).reduce((prev, curr) => {
|
|
72
|
+
return prev && !!curr.signedId;
|
|
73
|
+
}, true);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return false;
|
|
64
77
|
});
|
|
65
78
|
const uploadingFileLength = computed(() => Object.values(files.value).reduce((prev, curr) => {
|
|
66
79
|
if (curr.status == 'pending') prev = prev + 1;
|
|
@@ -35,6 +35,12 @@ export default {
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
methods: {
|
|
38
|
+
$values(namePrefix) {
|
|
39
|
+
return this.$parent.valuesInIndex(this.entryIndex, namePrefix);
|
|
40
|
+
},
|
|
41
|
+
$populateIndexes(object) {
|
|
42
|
+
return this.$parent.populateIndexes(object, this.entryIndex);
|
|
43
|
+
},
|
|
38
44
|
$prefixFieldName(fieldName) {
|
|
39
45
|
return this.$parent.prefixFieldName(this.entryIndex, fieldName);
|
|
40
46
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :style="$styles()" :class="classes()" v-if="loadIf">
|
|
2
|
+
<div :style="$styles()" :class="$classes()" v-if="loadIf">
|
|
3
3
|
<!-- See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date for why we need to use `pattern` -->
|
|
4
4
|
<v-text-field :color="gcolor" v-model="fieldModel" :name="fieldName" :label="spec.label" :hint="spec.hint"
|
|
5
5
|
:type="type" :readonly="spec.readOnly" :disabled="inputDisabled" :min="$sanitizeValue(spec.min)"
|
|
@@ -47,9 +47,6 @@ export default {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
|
-
classes() {
|
|
51
|
-
return this.$classes().concat("g-text-field--hintless");
|
|
52
|
-
},
|
|
53
50
|
onChange() {
|
|
54
51
|
this.$executeOnChange();
|
|
55
52
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div ref="container" :style="$styles()" :class="classes()" v-if="loadIf">
|
|
2
|
+
<div ref="container" :style="$styles()" :class="$classes()" v-if="loadIf">
|
|
3
3
|
<v-autocomplete :color="gcolor" v-model="fieldModel" :label="label" :items="normalizedOptions"
|
|
4
4
|
:chips="spec.multiple" :multiple="spec.multiple" :readonly="spec.readOnly" :clearable="!spec.readOnly"
|
|
5
5
|
:placeholder="spec.placeholder" :rules="rules" persistent-hint :append-icon="append.icon" validate-on="blur"
|
|
@@ -103,9 +103,6 @@ export default {
|
|
|
103
103
|
$ready() {
|
|
104
104
|
this.updateData(false);
|
|
105
105
|
},
|
|
106
|
-
classes() {
|
|
107
|
-
return this.$classes().concat("g-text-field--hintless");
|
|
108
|
-
},
|
|
109
106
|
updateData(reinitValue) {
|
|
110
107
|
// this.options = this.normalizedOptions();
|
|
111
108
|
this.append = this.spec.append || {};
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
<script>
|
|
21
21
|
import DynamicGroupEntryField from "./_dynamicGroupEntry.vue";
|
|
22
|
+
import { fieldModels } from "../composable/conditional";
|
|
22
23
|
|
|
23
24
|
export default {
|
|
24
25
|
components: {
|
|
@@ -91,10 +92,10 @@ export default {
|
|
|
91
92
|
});
|
|
92
93
|
return fieldProperties;
|
|
93
94
|
},
|
|
94
|
-
// Replace every occurrence of `{{
|
|
95
|
+
// Replace every occurrence of `{{entry_index}}` within any string.
|
|
95
96
|
populateIndexes(object, groupIndex) {
|
|
96
97
|
if (this.$type.isString(object)) {
|
|
97
|
-
return object.replace('{{
|
|
98
|
+
return object.replace('{{entry_index}}', groupIndex);
|
|
98
99
|
}
|
|
99
100
|
if (this.$type.isArray(object)) {
|
|
100
101
|
return object.map((o) => this.populateIndexes(o, groupIndex));
|
|
@@ -120,6 +121,17 @@ export default {
|
|
|
120
121
|
return `${this.spec.name}[${groupIndex}][${fieldName}]`;
|
|
121
122
|
}
|
|
122
123
|
},
|
|
124
|
+
valuesInIndex(groupIndex, namePrefix) {
|
|
125
|
+
const result = { [`${namePrefix}[_index]`]: groupIndex }
|
|
126
|
+
const groupPrefix = `${this.spec.name}[${groupIndex}]`;
|
|
127
|
+
for (const key in fieldModels) {
|
|
128
|
+
if (key.startsWith(groupPrefix)) {
|
|
129
|
+
const name = namePrefix + key.slice(groupPrefix.length);
|
|
130
|
+
result[name] = fieldModels[key] || "" // Avoid sending `undefined` or `null` to server as String
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return result
|
|
134
|
+
},
|
|
123
135
|
processField(viewSpec, name, groupIndex, properties) {
|
|
124
136
|
const fullName = this.prefixFieldName(groupIndex, name);
|
|
125
137
|
// const fullName = name;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :style="$styles()" :class="classes()">
|
|
2
|
+
<div :style="$styles()" :class="$classes()">
|
|
3
3
|
<div class="country-code">
|
|
4
4
|
<v-select v-model="countryCode" :items="sortedCountries" :readonly="spec.readOnly"
|
|
5
5
|
:outlined="$classes().includes('outlined') || null" :rounded="$classes().includes('rounded')"
|
|
6
|
-
:density="$classes().includes('compact') ? 'compact' : 'default'" item-text="name" item-value="iso2"
|
|
7
|
-
@change="onChangeCountryCode">
|
|
6
|
+
:density="$classes().includes('compact') ? 'compact' : 'default'" item-text="name" item-value="iso2"
|
|
7
|
+
return-object @change="onChangeCountryCode">
|
|
8
8
|
<!-- <template v-slot:selection> -->
|
|
9
9
|
<div :class="activeCountry.iso2.toLowerCase()" class="country_flag" />
|
|
10
10
|
<!-- </template> -->
|
|
@@ -166,9 +166,6 @@ export default {
|
|
|
166
166
|
hiddenDisplay() {
|
|
167
167
|
return this.$styles()["display"] == "none";
|
|
168
168
|
},
|
|
169
|
-
classes() {
|
|
170
|
-
return this.$classes().concat(["g-text-field--hintless", "fields-phone"]);
|
|
171
|
-
},
|
|
172
169
|
initializeCountry() {
|
|
173
170
|
return new Promise((resolve) => {
|
|
174
171
|
// 1. If the phone included prefix (+12), try to get the country and set it
|
|
@@ -277,20 +274,6 @@ export default {
|
|
|
277
274
|
|
|
278
275
|
<style src="./sprite.css"></style>
|
|
279
276
|
<style lang="scss">
|
|
280
|
-
.g-text-field--hintless {
|
|
281
|
-
.v-text-field {
|
|
282
|
-
.v-text-field__details {
|
|
283
|
-
min-height: 0;
|
|
284
|
-
margin-bottom: 0;
|
|
285
|
-
|
|
286
|
-
.v-messages__message {
|
|
287
|
-
min-height: 0;
|
|
288
|
-
line-height: 14px;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
277
|
.country_flag {
|
|
295
278
|
margin-right: 8px;
|
|
296
279
|
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
:outlined="$classes().includes('outlined') || null" type="text" :name="imageUploader.name"
|
|
23
23
|
:value="images[imageKey]" />
|
|
24
24
|
</div>
|
|
25
|
-
<input type="hidden" :name="
|
|
25
|
+
<input type="hidden" :name="fieldName" :value="producedValue" :disabled="inputDisabled" />
|
|
26
26
|
</div>
|
|
27
27
|
</template>
|
|
28
28
|
|
|
@@ -134,10 +134,6 @@ export default {
|
|
|
134
134
|
passwordField.appendIcon = "visibility";
|
|
135
135
|
}
|
|
136
136
|
},
|
|
137
|
-
classes() {
|
|
138
|
-
// return this.$classes().concat("g-text-field--hintless");
|
|
139
|
-
return this.$classes();
|
|
140
|
-
},
|
|
141
137
|
action_focus() {
|
|
142
138
|
this.$refs.field.focus();
|
|
143
139
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :style="styles()" :class="classes()" v-if="loadIf">
|
|
2
|
+
<div :style="styles()" :class="$classes()" v-if="loadIf">
|
|
3
3
|
<v-textarea :color="gcolor" v-model="fieldModel" :label="spec.label" :name="fieldName" :hint="spec.hint"
|
|
4
4
|
:placeholder="spec.placeholder" :maxlength="spec.maxLength || 255" :readonly="spec.readOnly" :height="height"
|
|
5
5
|
:rules="$validation()" counter :outlined="$classes().includes('outlined')" :disabled="inputDisabled"
|
|
@@ -38,9 +38,6 @@ export default {
|
|
|
38
38
|
this.height = styles.remove("height");
|
|
39
39
|
return styles;
|
|
40
40
|
},
|
|
41
|
-
classes() {
|
|
42
|
-
return this.$classes().concat("g-text-field--hintless");
|
|
43
|
-
},
|
|
44
41
|
onChange: eventFiltering.debounce(function () {
|
|
45
42
|
this.$executeOnChange();
|
|
46
43
|
}, 300)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :style="$styles()" :class="classes()" v-if="loadIf">
|
|
2
|
+
<div :style="$styles()" :class="$classes()" v-if="loadIf">
|
|
3
3
|
<div v-if="days_mode">
|
|
4
4
|
<div align="center">
|
|
5
5
|
<v-row align="center" justify="center">
|
|
@@ -106,9 +106,6 @@ export default {
|
|
|
106
106
|
this.fieldModel = value;
|
|
107
107
|
});
|
|
108
108
|
},
|
|
109
|
-
classes() {
|
|
110
|
-
return this.$classes().concat("g-text-field--hintless");
|
|
111
|
-
},
|
|
112
109
|
// days_mode methods
|
|
113
110
|
formatNum: num => (num < 10 ? "0" + num : num),
|
|
114
111
|
startCountdownTimer() {
|
|
@@ -3,8 +3,6 @@ import { fieldModels, watchFieldModels } from "../composable/conditional";
|
|
|
3
3
|
import { dirtySpecs } from "../composable/dirtyState";
|
|
4
4
|
import { determineColor } from "../../utils/constant";
|
|
5
5
|
import Action from "../../action";
|
|
6
|
-
import { inject, provide } from "vue";
|
|
7
|
-
|
|
8
6
|
const NUMBER_PRECISION = 2;
|
|
9
7
|
const isNeedToBeFixed = (val, component) => {
|
|
10
8
|
return component.type == 'number' && !Number.isInteger(val) && Number.isFinite(val);
|
|
@@ -294,17 +292,7 @@ export default {
|
|
|
294
292
|
vm.fieldModel = newSpec.value;
|
|
295
293
|
if (Utils.type.isNotNull(newSpec.displayed)) vm._show = newSpec.displayed;
|
|
296
294
|
vm._submitWhenNotDisplayed = newSpec.submitWhenNotDisplayed;
|
|
297
|
-
|
|
298
|
-
// if (vm.$el) {
|
|
299
|
-
// const els = vm.$el.querySelectorAll('input,textarea');
|
|
300
|
-
// if (!vm.submitWhenNotDisplayed) {
|
|
301
|
-
// els.forEach((el) => el.disabled = true);
|
|
302
|
-
// } else {
|
|
303
|
-
// els.forEach((el) => el.disabled = false);
|
|
304
|
-
// }
|
|
305
|
-
// }
|
|
306
|
-
|
|
307
|
-
Object.assign(this.spec, newSpec);
|
|
295
|
+
Object.assign(vm.spec, newSpec);
|
|
308
296
|
},
|
|
309
297
|
$classes(spec, defaultViewName) {
|
|
310
298
|
const properties = Object.assign(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component :is="componentName" :class="cssClasses" :style="cssStyles" :href="$href()" @click="$onClick()">
|
|
3
|
-
<template v-for="(item, index) in
|
|
3
|
+
<template v-for="(item, index) in latestSpec.childViews" :key="viewKey(item, index)">
|
|
4
4
|
<!-- Use view name for key to avoid component reuse issue -->
|
|
5
5
|
<glib-component :spec="item" />
|
|
6
6
|
</template>
|
|
@@ -13,6 +13,16 @@ export default {
|
|
|
13
13
|
spec: { type: Object, required: true }
|
|
14
14
|
},
|
|
15
15
|
computed: {
|
|
16
|
+
latestSpec() {
|
|
17
|
+
const spec = this.spec;
|
|
18
|
+
const data = GLib.component.findDataById(this.viewId);
|
|
19
|
+
if (data) {
|
|
20
|
+
Object.assign(spec, data);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return spec;
|
|
24
|
+
// return this.spec;
|
|
25
|
+
},
|
|
16
26
|
cssClasses: function () {
|
|
17
27
|
// TODO: Remove this after migrating to `panels/responsive`
|
|
18
28
|
// Vertical panels are nameless when used in predefined layout (e.g. page.body, list.header, etc.)
|
package/components/validation.js
CHANGED
|
@@ -104,9 +104,9 @@ class InclusionValidator extends AbstractValidator {
|
|
|
104
104
|
build(model) {
|
|
105
105
|
if (this.isAllowBlank() && !isPresent(model)) return true;
|
|
106
106
|
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
return
|
|
107
|
+
const value = new Set([model].flat());
|
|
108
|
+
const within = new Set(this.validationOptions.in || this.validationOptions.within);
|
|
109
|
+
return value.isSubsetOf(within) ? true : this.validationOptions.message;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
@@ -114,9 +114,9 @@ class ExclusionValidator extends AbstractValidator {
|
|
|
114
114
|
build(model) {
|
|
115
115
|
if (this.isAllowBlank() && !isPresent(model)) return true;
|
|
116
116
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
return !
|
|
117
|
+
const value = new Set([model].flat());
|
|
118
|
+
const within = new Set(this.validationOptions.in || this.validationOptions.within);
|
|
119
|
+
return !value.isSubsetOf(within) ? true : this.validationOptions.message;
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { testPageUrl } from "../../helper"
|
|
2
|
+
const url = testPageUrl('auto_validate')
|
|
3
|
+
|
|
4
|
+
describe('autoValidate', () => {
|
|
5
|
+
it('generate validate from model', () => {
|
|
6
|
+
cy.visit(url)
|
|
7
|
+
|
|
8
|
+
cy.contains('submit').click()
|
|
9
|
+
|
|
10
|
+
cy.get('.v-dialog').contains('OK').click()
|
|
11
|
+
|
|
12
|
+
cy.contains('This job is for person end with Doe').should('exist')
|
|
13
|
+
cy.contains('Say hello!').should('exist')
|
|
14
|
+
cy.contains('must be greater than or equal to 18').should('exist')
|
|
15
|
+
cy.contains('Too short').should('exist')
|
|
16
|
+
cy.contains('is too short (minimum is 1 character)').should('exist')
|
|
17
|
+
cy.contains('Min 1 word').should('exist')
|
|
18
|
+
cy.contains('We dont accept that kind of animal!').should('exist')
|
|
19
|
+
cy.contains('dont choose me').should('exist')
|
|
20
|
+
cy.contains('is not included in the list').should('exist')
|
|
21
|
+
cy.get('.fields-check .v-input--error').should('exist')
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { testPageUrl } from "../../helper"
|
|
2
|
+
const url = testPageUrl('dialog')
|
|
2
3
|
|
|
3
4
|
describe('dialog', () => {
|
|
4
5
|
it('updateExisting', () => {
|
|
5
|
-
cy.visit(
|
|
6
|
+
cy.visit(url)
|
|
6
7
|
cy.contains('Dialog updateExisting').click()
|
|
7
8
|
cy.get('.v-dialog h1').should('contain.text', 'Hello world')
|
|
8
9
|
cy.get('.v-dialog .v-icon').should('exist')
|
|
@@ -10,4 +11,15 @@ describe('dialog', () => {
|
|
|
10
11
|
cy.get('.v-dialog h1').should('contain.text', 'Hello world (updated)')
|
|
11
12
|
cy.get('.v-dialog .v-icon').should('not.exist')
|
|
12
13
|
})
|
|
14
|
+
|
|
15
|
+
it('open dialog', () => {
|
|
16
|
+
cy.visit(url)
|
|
17
|
+
cy.contains('Dialog open').click()
|
|
18
|
+
cy.get('.dialog-title > .v-btn').click()
|
|
19
|
+
|
|
20
|
+
cy.contains('Dialog open').click()
|
|
21
|
+
cy.get('.v-dialog').contains('close').click()
|
|
22
|
+
|
|
23
|
+
cy.get('.v-dialog').should('not.exist')
|
|
24
|
+
})
|
|
13
25
|
})
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { testPageUrl } from "../../helper"
|
|
2
|
+
const url = testPageUrl('form')
|
|
2
3
|
|
|
3
4
|
describe('display', () => {
|
|
4
5
|
it('form validity', () => {
|
|
5
|
-
cy.visit(
|
|
6
|
+
cy.visit(url)
|
|
6
7
|
|
|
7
8
|
cy.get('.fields-select .v-field__clearable').first().click()
|
|
8
9
|
cy.contains('submit (if form valid)').should('have.class', 'v-btn--disabled')
|
|
@@ -24,5 +25,31 @@ Form Data:
|
|
|
24
25
|
cy.get('.unformatted').should('contain.text', result)
|
|
25
26
|
})
|
|
26
27
|
|
|
28
|
+
it('form validity with same field name', () => {
|
|
29
|
+
cy.visit(url)
|
|
27
30
|
|
|
31
|
+
cy.contains('show jack').click()
|
|
32
|
+
|
|
33
|
+
// TODO: currently having form with 2 or more field the same name broke vuetify form validation
|
|
34
|
+
// cy.contains('submit (if form valid)').should('not.have.class', 'v-btn--disabled')
|
|
35
|
+
|
|
36
|
+
cy.contains('submit').click()
|
|
37
|
+
|
|
38
|
+
const result = `Method: POST
|
|
39
|
+
Form Data:
|
|
40
|
+
{
|
|
41
|
+
"date": "2024-07-24",
|
|
42
|
+
"select": [
|
|
43
|
+
"option1",
|
|
44
|
+
"option2"
|
|
45
|
+
],
|
|
46
|
+
"chip_group": "option2",
|
|
47
|
+
"radio_group": "option3",
|
|
48
|
+
"check_group": "option1",
|
|
49
|
+
"text": "Jack Doe",
|
|
50
|
+
"textarea": "Lorem ipsum et dumet bla bla bla..."
|
|
51
|
+
}`
|
|
52
|
+
|
|
53
|
+
cy.get('.unformatted').should('contain.text', result)
|
|
54
|
+
})
|
|
28
55
|
})
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { testPageUrl } from "../../helper"
|
|
2
|
+
const url = testPageUrl('form')
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
describe('reactivity', () => {
|
|
4
|
+
describe('form', () => {
|
|
5
5
|
it('logics/set, components/set, components/replace', () => {
|
|
6
|
-
cy.visit(
|
|
6
|
+
cy.visit(url)
|
|
7
7
|
|
|
8
8
|
// submit button should be clickabl e
|
|
9
9
|
cy.contains('submit (if form valid)').should('not.have.class', 'v-btn--disabled')
|
|
@@ -37,12 +37,10 @@ Form Data:
|
|
|
37
37
|
"option99"
|
|
38
38
|
],
|
|
39
39
|
"chip_group": "option99",
|
|
40
|
-
"text": "Doe
|
|
40
|
+
"text": "John Doe",
|
|
41
41
|
"textarea": "The quick brown fox jumps over the lazy dog"
|
|
42
42
|
}`
|
|
43
43
|
|
|
44
44
|
cy.get('.unformatted').should('contain.text', result)
|
|
45
45
|
})
|
|
46
|
-
|
|
47
|
-
|
|
48
46
|
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { testPageUrl } from "../../helper"
|
|
2
|
+
const url = testPageUrl('form_dynamic')
|
|
3
|
+
|
|
4
|
+
describe('form', () => {
|
|
5
|
+
it('dynamic group', () => {
|
|
6
|
+
cy.visit(url)
|
|
7
|
+
|
|
8
|
+
cy.get('.text-success').click()
|
|
9
|
+
|
|
10
|
+
cy.contains('submit (if form valid)').should('have.class', 'v-btn--disabled')
|
|
11
|
+
|
|
12
|
+
cy.get('#input-23').type('Rate your breakfast')
|
|
13
|
+
cy.get('#input-25').click()
|
|
14
|
+
cy.get('.v-overlay-container').contains('Rating').click()
|
|
15
|
+
|
|
16
|
+
cy.contains('submit (if form valid)').should('not.have.class', 'v-btn--disabled')
|
|
17
|
+
|
|
18
|
+
cy.get(':nth-child(3) > [style="display: block;"] > .group-wrapper > .text-error').click()
|
|
19
|
+
|
|
20
|
+
cy.contains('Confirm').click()
|
|
21
|
+
|
|
22
|
+
cy.contains('submit').click()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const result = `Method: POST
|
|
26
|
+
Form Data:
|
|
27
|
+
{
|
|
28
|
+
"evaluation": {
|
|
29
|
+
"0": {
|
|
30
|
+
"question": "Punctuality",
|
|
31
|
+
"type": "rating",
|
|
32
|
+
"enabled": ""
|
|
33
|
+
},
|
|
34
|
+
"1": {
|
|
35
|
+
"question": "Quality of work",
|
|
36
|
+
"type": "rating",
|
|
37
|
+
"enabled": "1"
|
|
38
|
+
},
|
|
39
|
+
"2": {
|
|
40
|
+
"_destroy": "1",
|
|
41
|
+
"question": "Satisfied?",
|
|
42
|
+
"type": "yes_no"
|
|
43
|
+
},
|
|
44
|
+
"3": {
|
|
45
|
+
"question": "Rate your breakfast",
|
|
46
|
+
"type": "rating",
|
|
47
|
+
"enabled": ""
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}`
|
|
51
|
+
|
|
52
|
+
cy.get('.unformatted').should('contain.text', result)
|
|
53
|
+
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { testPageUrl } from "../../helper"
|
|
2
|
+
const url = testPageUrl('multiupload')
|
|
3
|
+
|
|
4
|
+
describe('multiUpload', () => {
|
|
5
|
+
it('can change files', () => {
|
|
6
|
+
cy.visit(url)
|
|
7
|
+
|
|
8
|
+
cy.contains('clear files').click()
|
|
9
|
+
cy.contains('populate files').click()
|
|
10
|
+
|
|
11
|
+
cy.contains('submit').click()
|
|
12
|
+
|
|
13
|
+
const result = `Method: POST
|
|
14
|
+
Form Data:
|
|
15
|
+
{
|
|
16
|
+
"multi2": [
|
|
17
|
+
"eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBUUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--29513eb922c80b9cfb11d387b885f70b03fead1b"
|
|
18
|
+
]
|
|
19
|
+
}`
|
|
20
|
+
|
|
21
|
+
cy.get('.unformatted').should('contain.text', result)
|
|
22
|
+
})
|
|
23
|
+
})
|
package/cypress/helper.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const DEV_TEST_PAGE = 'http://localhost:3000/glib/json_ui_garage?path=test_page%
|
|
1
|
+
const DEV_TEST_PAGE = 'http://localhost:3000/glib/json_ui_garage?path=test_page%2F{{testPage}}'
|
|
2
2
|
|
|
3
|
-
function testPageUrl() {
|
|
4
|
-
return DEV_TEST_PAGE;
|
|
3
|
+
function testPageUrl(testPage) {
|
|
4
|
+
return DEV_TEST_PAGE.replace('{{testPage}}', testPage);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export { testPageUrl }
|
package/package.json
CHANGED
|
@@ -1,33 +1,58 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
install: (Vue, options) => {
|
|
3
3
|
Vue.mixin({
|
|
4
|
+
computed: {
|
|
5
|
+
viewId() {
|
|
6
|
+
if (this.spec && this.spec.id) {
|
|
7
|
+
const id = this.spec.id;
|
|
8
|
+
|
|
9
|
+
if (id.includes('{{entry_index}}')) {
|
|
10
|
+
const dynamicGroupEntry = this.$closest("fields/internalDynamicGroupEntry");
|
|
11
|
+
if (dynamicGroupEntry) {
|
|
12
|
+
return dynamicGroupEntry.$populateIndexes(id);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return id;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
4
20
|
methods: {
|
|
5
21
|
$ready() {
|
|
6
22
|
let spec = this.spec;
|
|
7
23
|
if (spec && spec.id && this.$registryEnabled()) {
|
|
8
|
-
|
|
9
|
-
const
|
|
24
|
+
|
|
25
|
+
const id = this.viewId;
|
|
26
|
+
|
|
27
|
+
// const id = spec.id;
|
|
28
|
+
// const existingComponent = GLib.component.findById(id, false);
|
|
10
29
|
// A component with the same ID in a different page shouldn't be considered a
|
|
11
30
|
// duplicate. See `utils/components#deregister` for more details.
|
|
12
|
-
if (existingComponent) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
31
|
+
// if (existingComponent) {
|
|
32
|
+
// console.warn(
|
|
33
|
+
// "Duplicate component ID:",
|
|
34
|
+
// id,
|
|
35
|
+
// "Existing:",
|
|
36
|
+
// GLib.component.vueName(existingComponent),
|
|
37
|
+
// "New:",
|
|
38
|
+
// GLib.component.vueName(this)
|
|
39
|
+
// );
|
|
40
|
+
// }
|
|
22
41
|
const newComponent = this.$refs.delegate || this;
|
|
23
42
|
GLib.component.register(id, newComponent);
|
|
24
43
|
}
|
|
25
44
|
},
|
|
26
45
|
$tearDown() {
|
|
27
46
|
let spec = this.spec;
|
|
47
|
+
|
|
28
48
|
if (spec && spec.id && this.$registryEnabled()) {
|
|
29
49
|
GLib.component.deregister(spec.id, this);
|
|
30
50
|
}
|
|
51
|
+
|
|
52
|
+
// Utils.type.ifString(this.viewId, (viewId) => {
|
|
53
|
+
// console.log("DEREGISTER DATA1", viewId)
|
|
54
|
+
// GLib.component.deregisterData(viewId);
|
|
55
|
+
// })
|
|
31
56
|
},
|
|
32
57
|
$registryEnabled() {
|
|
33
58
|
// Common classes such as `_select` need to return false so that it doesn't override its parent (e.g. `select`).
|
package/store.js
CHANGED
|
@@ -13,7 +13,8 @@ export const vueApp = reactive({
|
|
|
13
13
|
isBusy: false,
|
|
14
14
|
webSocket: { channels: {}, header: {} },
|
|
15
15
|
actionCable: null,
|
|
16
|
-
registeredComponents:
|
|
16
|
+
registeredComponents: {},
|
|
17
|
+
registeredComponentData: {},
|
|
17
18
|
temp: {},
|
|
18
19
|
richTextValues: {},
|
|
19
20
|
draggedComponent: null,
|
|
@@ -52,20 +53,26 @@ function glibEventHandler(e) {
|
|
|
52
53
|
closeAllDialog();
|
|
53
54
|
closeAllPopover();
|
|
54
55
|
|
|
55
|
-
const { onUnload } = jsonView.page;
|
|
56
|
-
if (onUnload) {
|
|
57
|
-
Action.execute(onUnload, {});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
56
|
if (e) {
|
|
61
57
|
if (ctx().isFormDirty && !ctx().isFormSubmitted) {
|
|
62
58
|
e.preventDefault();
|
|
63
59
|
}
|
|
64
60
|
} else {
|
|
65
|
-
|
|
61
|
+
const confirmUnload = !isDirty();
|
|
62
|
+
if (confirmUnload) {
|
|
63
|
+
// Clear after we go past dirty prompt.
|
|
64
|
+
GLib.component.clearData();
|
|
65
|
+
|
|
66
|
+
const { onUnload } = jsonView.page;
|
|
67
|
+
if (onUnload) {
|
|
68
|
+
Action.execute(onUnload, {});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return confirmUnload;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
}
|
|
75
|
+
|
|
69
76
|
export const watchGlibEvent = () => {
|
|
70
77
|
window.onbeforeunload = glibEventHandler;
|
|
71
78
|
glibevent.onbeforewindowsopen = glibEventHandler;
|
package/utils/component.js
CHANGED
|
@@ -7,25 +7,27 @@ export default class {
|
|
|
7
7
|
return vueApp.registeredComponents;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
static
|
|
10
|
+
static get _dataRegistry() {
|
|
11
|
+
return vueApp.registeredComponentData;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static findById(id, warn = true) {
|
|
15
|
+
if (!vueApp.registeredComponents[id] && warn) console.warn("Component ID not found", id);
|
|
11
16
|
return vueApp.registeredComponents[id];
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
static
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// "Duplicate component ID:",
|
|
18
|
-
// id,
|
|
19
|
-
// "Existing:",
|
|
20
|
-
// this.vueName(this._registry[id]),
|
|
21
|
-
// "New:",
|
|
22
|
-
// this.vueName(component)
|
|
23
|
-
// );
|
|
24
|
-
// }
|
|
19
|
+
static findDataById(id) {
|
|
20
|
+
return this._dataRegistry[id];
|
|
21
|
+
}
|
|
25
22
|
|
|
23
|
+
static register(id, component) {
|
|
26
24
|
this._registry[id] = component;
|
|
27
25
|
}
|
|
28
26
|
|
|
27
|
+
static registerData(id, data) {
|
|
28
|
+
this._dataRegistry[id] = data;
|
|
29
|
+
}
|
|
30
|
+
|
|
29
31
|
static deregister(id, component) {
|
|
30
32
|
// As the user navigates to a new page, there may be a component with the same ID in the next page,
|
|
31
33
|
// that is not reused from the component in the previous page. When this happens, the new
|
|
@@ -37,6 +39,17 @@ export default class {
|
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
static deregisterData(id) {
|
|
43
|
+
delete this._dataRegistry[id];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static clearData() {
|
|
47
|
+
console.debug("Clearing components' dynamic data");
|
|
48
|
+
for (const key in this._dataRegistry) {
|
|
49
|
+
this.deregisterData(key)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
static vueName(component) {
|
|
41
54
|
// _componentTag not found
|
|
42
55
|
// return component.$options._componentTag;
|