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.
Files changed (36) hide show
  1. package/README.md +5 -4
  2. package/action.js +4 -0
  3. package/actions/components/replace.js +4 -7
  4. package/actions/components/replaceChildren.js +1 -0
  5. package/actions/components/set.js +1 -5
  6. package/actions/dialogs/close.js +2 -0
  7. package/actions/fields/getDynamicGroupEntryValues.js +23 -0
  8. package/actions/fields/reset.js +1 -1
  9. package/actions/logics/run.js +35 -0
  10. package/actions/logics/set.js +12 -1
  11. package/app.vue +9 -0
  12. package/components/composable/file.js +15 -2
  13. package/components/fields/_dynamicGroupEntry.vue +6 -0
  14. package/components/fields/_patternText.vue +1 -4
  15. package/components/fields/_select.vue +1 -4
  16. package/components/fields/dynamicGroup.vue +14 -2
  17. package/components/fields/phone/field.vue +3 -20
  18. package/components/fields/richText.vue +1 -1
  19. package/components/fields/text.vue +0 -4
  20. package/components/fields/textarea.vue +1 -4
  21. package/components/fields/timer.vue +1 -4
  22. package/components/mixins/generic.js +1 -2
  23. package/components/mixins/styles.js +1 -13
  24. package/components/panels/vertical.vue +11 -1
  25. package/components/validation.js +6 -6
  26. package/cypress/e2e/glib-web/autoValidate.cy.ts +23 -0
  27. package/cypress/e2e/glib-web/dialog.cy.ts +13 -1
  28. package/cypress/e2e/glib-web/display.cy.ts +28 -1
  29. package/cypress/e2e/glib-web/{reactivity.cy.ts → form.cy.ts} +4 -6
  30. package/cypress/e2e/glib-web/formDynamic.cy.ts +55 -0
  31. package/cypress/e2e/glib-web/multiupload.cy.ts +23 -0
  32. package/cypress/helper.ts +3 -3
  33. package/package.json +2 -2
  34. package/plugins/updatableComponent.js +37 -12
  35. package/store.js +14 -7
  36. 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
- if (target) {
13
- target.action_merge(spec.newView);
14
- Action.execute(spec.onReplace, target);
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,4 +1,5 @@
1
1
  import { nextTick } from "vue";
2
+ import Action from "../../action";
2
3
 
3
4
  // Experimental
4
5
  export default class {
@@ -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
- // if (data.value) setFileModel(targetComponent, data.value);
12
+
17
13
  });
18
14
 
19
15
  GLib.action.execute(spec.onSet, targetComponent);
@@ -1,3 +1,5 @@
1
+ import { dialogs } from "../../store";
2
+
1
3
  export default class {
2
4
  execute(spec, component) {
3
5
  Utils.launch.dialog.close(spec, component);
@@ -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
+ }
@@ -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
+ }
@@ -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
@@ -331,6 +331,15 @@ body,
331
331
  pointer-events: none;
332
332
  }
333
333
 
334
+ .v-input .v-input__details {
335
+ min-height: fit-content;
336
+ padding-top: 0px;
337
+
338
+ .v-messages__message {
339
+ padding-top: 6px;
340
+ }
341
+ }
342
+
334
343
  /******/
335
344
  </style>
336
345
 
@@ -57,10 +57,23 @@ function useFileUtils() {
57
57
 
58
58
  function useFilesState(files) {
59
59
  const uploading = computed(() => {
60
- return Object.values(files.value).length > 0 ? Object.values(files.value).reduce((prev, curr) => prev || curr.status == 'pending', false) : false;
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
- return Object.values(files.value).length > 0 ? Object.values(files.value).reduce((prev, curr) => prev && !!curr.signedId, true) : false;
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 `{{index}}` within any string.
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('{{index}}', groupIndex);
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" return-object
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="spec.name" :value="producedValue" :disabled="inputDisabled" />
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() {
@@ -16,9 +16,8 @@ export default {
16
16
  parent.$componentName() == name &&
17
17
  parent.$registryEnabled()
18
18
  ) {
19
- return parent;
19
+ return parent.$refs.delegate ? parent.$refs.delegate : parent;
20
20
  }
21
-
22
21
  parent = parent.$parent;
23
22
  }
24
23
  },
@@ -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 spec.childViews" :key="viewKey(item, index)">
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.)
@@ -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 within = this.validationOptions.in || this.validationOptions.within;
108
-
109
- return within.includes(model) ? true : this.validationOptions.message;
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 within = this.validationOptions.in || this.validationOptions.within;
118
-
119
- return !within.includes(model) ? true : this.validationOptions.message;
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(testPageUrl())
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(testPageUrl())
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(testPageUrl())
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 John",
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%2Findex'
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,10 +1,10 @@
1
1
  {
2
2
  "name": "glib-web",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "cypress run"
7
+ "test": "cypress run --browser chrome"
8
8
  },
9
9
  "author": "",
10
10
  "license": "ISC",
@@ -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
- const id = spec.id;
9
- const existingComponent = GLib.component.findById(id);
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
- console.warn(
14
- "Duplicate component ID:",
15
- id,
16
- "Existing:",
17
- GLib.component.vueName(existingComponent),
18
- "New:",
19
- GLib.component.vueName(this)
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
- return !isDirty();
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;
@@ -7,25 +7,27 @@ export default class {
7
7
  return vueApp.registeredComponents;
8
8
  }
9
9
 
10
- static findById(id) {
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 register(id, component) {
15
- // if (this._registry[id]) {
16
- // console.warn(
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;