glib-web 4.1.0 → 4.1.2

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 (48) hide show
  1. package/.eslintrc.js +4 -1
  2. package/README.md +1 -0
  3. package/action.js +1 -1
  4. package/actions/components/replace.js +1 -1
  5. package/actions/components/set.js +4 -4
  6. package/actions/dialogs/open.js +1 -1
  7. package/actions/dialogs/show.js +1 -1
  8. package/components/fields/_patternText.vue +1 -1
  9. package/components/fields/_select.vue +2 -2
  10. package/components/fields/check.vue +1 -1
  11. package/components/fields/checkGroup.vue +3 -2
  12. package/components/fields/chipGroup.vue +5 -3
  13. package/components/fields/creditCard.vue +1 -1
  14. package/components/fields/dynamicGroup.vue +1 -1
  15. package/components/fields/dynamicSelect.vue +1 -1
  16. package/components/fields/file.vue +1 -1
  17. package/components/fields/googlePlace.vue +1 -1
  18. package/components/fields/hidden.vue +1 -1
  19. package/components/fields/location.vue +1 -1
  20. package/components/fields/multiUpload.vue +1 -1
  21. package/components/fields/otpField.vue +1 -1
  22. package/components/fields/phone.vue +1 -1
  23. package/components/fields/radio.vue +111 -4
  24. package/components/fields/radioGroup.vue +1 -1
  25. package/components/fields/rating.vue +1 -1
  26. package/components/fields/richText.vue +1 -1
  27. package/components/fields/sign.vue +1 -1
  28. package/components/fields/stripe/stripeFields.vue +1 -1
  29. package/components/fields/stripe/stripeIndividualFields.vue +1 -1
  30. package/components/fields/stripeExternalAccount.vue +1 -1
  31. package/components/fields/stripeToken.vue +1 -1
  32. package/components/fields/text.vue +1 -1
  33. package/components/fields/textarea.vue +1 -1
  34. package/components/fields/timer.vue +1 -1
  35. package/components/mixins/generic.js +9 -3
  36. package/components/mixins/styles.js +31 -7
  37. package/cypress/e2e/glib-web/dialog.cy.ts +13 -0
  38. package/cypress/e2e/glib-web/display.cy.ts +12 -0
  39. package/cypress/e2e/glib-web/reactivity.cy.ts +48 -0
  40. package/cypress/helper.ts +7 -0
  41. package/nav/dialog.vue +7 -8
  42. package/package.json +1 -1
  43. package/store.js +18 -5
  44. package/utils/http.js +1 -1
  45. package/utils/launch.js +3 -3
  46. package/cypress/e2e/glib-web/components_set.cy.ts +0 -8
  47. package/cypress/e2e/glib-web/picker.cy.ts +0 -9
  48. package/cypress/e2e/glib-web/select.cy.ts +0 -57
package/.eslintrc.js CHANGED
@@ -33,6 +33,9 @@ module.exports = {
33
33
  "GLib": "readonly",
34
34
  "Utils": "readonly",
35
35
  "google": "readonly",
36
- "Stripe": "readonly"
36
+ "Stripe": "readonly",
37
+ "cy": "readonly",
38
+ "it": "readonly",
39
+ "describe": "readonly"
37
40
  }
38
41
  };
package/README.md CHANGED
@@ -61,6 +61,7 @@ view.chipGroup styleClasses: ['custom']
61
61
  ## Prepare for publishing
62
62
 
63
63
  - Create a PR to the `master` branch
64
+ - Make sure test pass `yarn run test`
64
65
  - After approval, merge the PR.
65
66
 
66
67
  ## Publish package to npm
package/action.js CHANGED
@@ -228,7 +228,7 @@ export default class Action {
228
228
 
229
229
  if (response.header || response.body || response.footer) {
230
230
  Utils.http.forceComponentUpdate(() => {
231
- const dialog = dialogs.last();
231
+ const dialog = dialogs.value.last();
232
232
  const updateDialog = windowMode ? false : Utils.type.isObject(dialog);
233
233
  if (updateDialog) {
234
234
  dialog.updateContent(response);
@@ -5,7 +5,7 @@ export default class {
5
5
  execute(spec, component) {
6
6
  const target = GLib.component.findById(spec.targetId) || component;
7
7
  if (target) {
8
- Object.assign(target.spec, spec.newView);
8
+ target.action_merge(spec.newView);
9
9
  }
10
10
 
11
11
  Action.execute(spec.onReplace, target);
@@ -1,6 +1,6 @@
1
- const setFileModel = (component, value) => {
2
- component.fieldModel = component.$sanitizeValue(component.$externalizeValue(value));
3
- };
1
+ // const setFileModel = (component, value) => {
2
+ // component.fieldModel = component.$sanitizeValue(component.$externalizeValue(value));
3
+ // };
4
4
 
5
5
 
6
6
  export default class {
@@ -13,7 +13,7 @@ export default class {
13
13
 
14
14
  Utils.type.ifObject(spec.data, (data) => {
15
15
  targetComponent.action_merge(data);
16
- if (data.value) setFileModel(targetComponent, data.value);
16
+ // if (data.value) setFileModel(targetComponent, data.value);
17
17
  });
18
18
 
19
19
  GLib.action.execute(spec.onSet, targetComponent);
@@ -2,7 +2,7 @@ import { dialogs } from "../../store";
2
2
 
3
3
  export default class {
4
4
  execute(spec, component) {
5
- const dialog = dialogs.last();
5
+ const dialog = dialogs.value.last();
6
6
  if (spec.updateExisting && dialog) {
7
7
  dialog.reload(spec);
8
8
 
@@ -2,7 +2,7 @@ import { dialogs } from "../../store";
2
2
 
3
3
  export default class {
4
4
  execute(spec, component) {
5
- const dialog = dialogs.last();
5
+ const dialog = dialogs.value.last();
6
6
  if (spec.updateExisting && dialog) {
7
7
  dialog.updateContent(spec);
8
8
  return;
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="classes()">
2
+ <div :style="$styles()" :class="classes()" v-if="!isInputIgnored">
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)"
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div ref="container" :style="$styles()" :class="classes()">
2
+ <div ref="container" :style="$styles()" :class="classes()" v-if="!isInputIgnored">
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"
@@ -49,7 +49,7 @@ export default {
49
49
  return {
50
50
  options: null,
51
51
  append: {},
52
- rules: [],
52
+ rules: this.$validation(),
53
53
  focused: false
54
54
  };
55
55
  },
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :class="$classes()" :style="$styles()">
2
+ <div :class="$classes()" :style="$styles()" v-if="!isInputIgnored">
3
3
  <!-- This hidden field should always be there to make sure the submitted param is not empty,
4
4
  which could cause "Not accessible" error on the server. -->
5
5
  <input v-model="uncheckValue" type="hidden" :name="fieldName" />
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()">
2
+ <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
3
3
  <fields-check v-for="(childView, index) in childViews" :spec="childView" :key="index"
4
4
  @oncheck="updateFieldModel"></fields-check>
5
5
  <v-input ref="validator" :model-value="fieldModel" :rules="$validation()" :name="fieldName" validate-on="input"
@@ -31,7 +31,8 @@ export default defineComponent({
31
31
  return this.spec.childViews.map((childView) => {
32
32
  const { readOnly, disabled } = this.spec;
33
33
  const parentName = this.spec.name;
34
- return Object.assign({}, { readOnly, disabled, parentName }, childView);
34
+ const value = [this.spec.value].flat().includes(childView.checkValue) ? childView.checkValue : null;
35
+ return Object.assign({}, { readOnly, disabled, parentName, value }, childView);
35
36
  });
36
37
  }
37
38
  },
@@ -1,9 +1,11 @@
1
1
  <template>
2
- <div ref="container" :style="$styles()" :class="$classes()">
2
+ <div ref="container" :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
3
3
  <v-chip-group :color="gcolor" v-model="fieldModel" @update:modelValue="onChange()" :multiple="spec.multiple" column>
4
4
  <v-chip v-for="(option, index) in spec.options" :key="index" filter :variant="variant">{{
5
5
  option.text }}</v-chip>
6
6
  </v-chip-group>
7
+ <v-input ref="validator" :model-value="fieldModel" :rules="$validation()" :name="fieldName" validate-on="input"
8
+ :disabled="inputDisabled"></v-input>
7
9
  <input v-for="(item, index) in indexToValue(fieldModel)" :key="index" type="hidden" :disabled="inputDisabled"
8
10
  :name="fieldName" :value="item" />
9
11
  </div>
@@ -12,8 +14,7 @@
12
14
  <script>
13
15
  import { defineComponent } from "vue";
14
16
  import inputVariant from "../mixins/inputVariant";
15
- import { triggerOnInput } from "../composable/form";
16
- import { fieldModels } from "../composable/conditional";
17
+ import { triggerOnChange, triggerOnInput } from "../composable/form";
17
18
 
18
19
  export default defineComponent({
19
20
  mixins: [inputVariant],
@@ -26,6 +27,7 @@ export default defineComponent({
26
27
  const containerEl = this.$refs.container;
27
28
  if (containerEl) {
28
29
  triggerOnInput(containerEl);
30
+ triggerOnChange(containerEl);
29
31
  }
30
32
  },
31
33
  indexToValue(value) {
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()">
2
+ <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
3
3
  <div class="stretcher">
4
4
  <StripeIndividualFields v-if="$classes().includes('individual')" :spec="spec" ref="delegate"
5
5
  :on-complete="_retrieveToken" :on-error="_displayError" />
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()">
2
+ <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
3
3
  <div v-for="(group, groupIndex) in groupSpecs" :key="`group${groupIndex}`">
4
4
  <input v-if="isDeleted(groupIndex)" type="hidden" :name="`${spec.name}[${groupIndex}][_destroy]`" value="1" />
5
5
  <div :style="{ display: isDeleted(groupIndex) ? 'none' : 'block' }">
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()">
2
+ <div :style="$styles()" v-if="!isInputIgnored">
3
3
  <v-autocomplete ref="autocomplete" v-model="model" :items="allItems" :loading="$isBusy"
4
4
  :v-model:search-input="search" :label="spec.label" hide-no-data hide-selected no-filter return-object
5
5
  :chips="true" :deletable-chips="true" :multiple="spec.multiple">
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div ref="fileUploadContainer" :style="$styles()" :class="$classes()">
2
+ <div ref="fileUploadContainer" :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
3
3
  <label v-if="spec.label" class="v-label v-label--active">{{ spec.label }}</label>
4
4
  <div class="preview-container" :class="[fileTitle ? 'uploaded' : '']">
5
5
  <v-avatar v-if="placeholder.type == 'avatar'" :style="$styles(placeholder)">
@@ -1,7 +1,7 @@
1
1
  <!-- TODO: This probably can be merged with latLong-v1 or map-v1, i.e. add infoWindow support to latLong or map -->
2
2
 
3
3
  <template>
4
- <v-container fluid class="pa-0">
4
+ <v-container fluid class="pa-0" v-if="!isInputIgnored">
5
5
  <div class="v-input v-text-field theme--light">
6
6
  <div class="v-input__control">
7
7
  <div class="v-input__slot">
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <input type="hidden" :name="fieldName" :value="fieldModel" :disabled="inputDisabled" />
2
+ <input type="hidden" :name="fieldName" :value="fieldModel" :disabled="inputDisabled" v-if="!isInputIgnored" />
3
3
  </template>
4
4
 
5
5
  <script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()">
2
+ <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
3
3
 
4
4
  <fields-text ref="autocomplete" type="text" :spec="spec"></fields-text>
5
5
 
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()">
2
+ <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
3
3
  <div ref="container" @click="handleClick" @drop="handleDrop" @dragover="handleDragOver" @dragleave="handleDragLeave"
4
4
  class="gdrop-file border-[2px]">
5
5
  <input ref="fileSelect" type="file" multiple style="display: none">
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div ref="otp" :style="$styles()" :class="$classes()">
2
+ <div ref="otp" :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
3
3
  <v-otp-input ref="field" v-model="fieldModel" :name="fieldName" :disabled="inputDisabled" :rounded="4"
4
4
  :max-width="maxWidth" type="spec.type || 'number'" :length="length" :variant="variant"
5
5
  @change="$executeOnChange()" />
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()">
2
+ <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
3
3
  <v-phone-input :guessCountry="guessCountry" displayFormat="e164" :defaultCountry="defaultCountry" :color="gcolor"
4
4
  v-model="fieldModel" :label="spec.label" :name="fieldName" :hint="spec.hint" :placeholder="spec.placeholder"
5
5
  :maxlength="spec.maxLength || 255" :readonly="spec.readOnly" :rules="$validation()"
@@ -1,9 +1,20 @@
1
1
  <template>
2
- <div :class="$classes()" :style="$styles()">
3
- <v-radio :label="spec.label" :value="spec.value.presence() || vuetifyEmptyString" :readonly="spec.readOnly"
4
- :on-icon="spec.onIcon" :off-icon="spec.offIcon" @click="$onClick()" :color="gcolor" />
2
+ <div :class="$classes()" :style="$styles()" v-if="!isInputIgnored">
3
+ <v-radio :label="spec.label" :value="(spec.value || '').presence() || vuetifyEmptyString" :readonly="spec.readOnly"
4
+ :on-icon="spec.onIcon" :off-icon="spec.offIcon" @click="$onClick()" :color="gcolor"
5
+ :class="spec.imageUrl || spec.icon ? 'custom-radio' : ''">
6
+ <template v-slot:label v-if="spec.imageUrl || spec.icon">
7
+ <div class="custom-radio-content">
8
+ <img v-if="spec.imageUrl" :src="spec.imageUrl" alt="icon" class="custom-radio-icon" />
9
+ <v-icon v-if="spec.icon" class="aligner" :color="spec.iconColor" :size="spec.iconSize">
10
+ {{ spec.icon }}
11
+ </v-icon>
12
+ <div class="custom-radio-label">{{ spec.label }}</div>
13
+ </div>
14
+ </template>
15
+ </v-radio>
5
16
  <div v-if="spec.childViews" class="radio-childviews">
6
- <div v-for="(item, i) in spec.childViews" :key="i">
17
+ <div v-for="( item, i ) in spec.childViews " :key="i">
7
18
  <glib-component :spec="item" />
8
19
  </div>
9
20
  </div>
@@ -18,3 +29,99 @@ export default {
18
29
  }
19
30
  };
20
31
  </script>
32
+
33
+ <style scoped>
34
+ .custom-radio {
35
+ /* styles for custom-radio class */
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ width: 240px;
40
+ height: 254px;
41
+ transition: border-color 0.3s, box-shadow 0.3s, color 0.3s;
42
+ text-align: center;
43
+ cursor: pointer;
44
+ padding: 16px;
45
+ position: relative;
46
+ border: 1px solid #E6E6E6;
47
+ border-radius: 24px;
48
+ }
49
+
50
+ .custom-radio:hover {
51
+ border: 2px solid;
52
+ border-radius: 24px;
53
+ border-color: #0A2A9E;
54
+ }
55
+
56
+ .custom-radio:hover .custom-radio-label {
57
+ font-size: 22px;
58
+ color: #0A2A9E;
59
+ }
60
+
61
+ .custom-radio .custom-radio-content {
62
+ display: flex;
63
+ flex-direction: column;
64
+ align-items: center;
65
+ justify-content: center;
66
+ width: 100%;
67
+ height: 100%;
68
+ margin-right: 30px;
69
+ }
70
+
71
+ .custom-radio .custom-radio-icon {
72
+ width: 80px;
73
+ height: 80px;
74
+ margin-bottom: 8px;
75
+ }
76
+
77
+ .custom-radio .custom-radio-label {
78
+ font-size: 22px;
79
+ color: inherit;
80
+ margin-top: 16px;
81
+ }
82
+
83
+ .custom-radio ::v-deep .v-selection-control__input {
84
+ width: 240px;
85
+ height: 254px;
86
+ background-color: transparent;
87
+ left: 50%;
88
+
89
+ >.v-icon {
90
+ opacity: 0;
91
+ }
92
+
93
+ }
94
+
95
+ .custom-radio ::v-deep .v-selection-control__input::before {
96
+ background-color: transparent
97
+ }
98
+
99
+ .custom-radio ::v-deep .v-ripple__container {
100
+ display: none;
101
+ }
102
+
103
+
104
+ .custom-radio .v-ripple__container {
105
+ display: none;
106
+ }
107
+
108
+ .radio--active .custom-radio {
109
+ border: 2px solid;
110
+ border-radius: 24px;
111
+ border-color: #0A2A9E;
112
+ }
113
+
114
+ .radio--active .custom-radio-label {
115
+ color: #0A2A9E;
116
+ font-weight: 700;
117
+ font-size: 22px;
118
+ }
119
+
120
+ .radio--active .v-icon {
121
+ color: #0A2A9E;
122
+ }
123
+
124
+ .radio-childviews {
125
+ margin-top: 16px;
126
+ }
127
+ </style>
@@ -5,7 +5,7 @@
5
5
  -->
6
6
  <v-radio-group v-model="fieldModel" :name="fieldName" :readonly="spec.readOnly" :disabled="inputDisabled"
7
7
  :rules="$validation()" :inline="spec.row" validate-on="blur" @change="$executeOnChange()" :class="$classes()"
8
- :style="$styles()">
8
+ :style="$styles()" v-if="!isInputIgnored">
9
9
  <div v-for="(childView, index) in spec.childViews" :key="index" style="width: 100%;">
10
10
  <glib-component :spec="childSpec(childView)" />
11
11
  </div>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div>
2
+ <div v-if="!isInputIgnored">
3
3
  <v-rating v-model="fieldModel" :name="fieldName" empty-icon="star_outline" full-icon="star" half-icon="star_half"
4
4
  :half-increments="spec.halfIncrements" hover length="5" :readonly="spec.readOnly" :disabled="inputDisabled"
5
5
  :color="spec.color" :bg-color="spec.color" :size="spec.size"></v-rating>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div ref="container" :style="$styles()" :class="$classes()">
2
+ <div ref="container" :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
3
3
  <v-tabs v-model="mode" fixed-tabs>
4
4
  <v-tab @click="onRichTextClicked">Editor</v-tab>
5
5
  <v-tab @click="onRawTextClicked">Code</v-tab>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :class="$classes()" :style="$styles()">
2
+ <div :class="$classes()" :style="$styles()" v-if="!isInputIgnored">
3
3
  <div class="glib-canvas-container">
4
4
  <canvas style="width: 100%; height: 100%" @mousedown="handleDrawStart" @mousemove="handleDrawing"
5
5
  @mouseup="handleDrawEnd" @touchstart="handleDrawStart" @touchmove="handleDrawing" @touchend="handleDrawEnd"
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div>
3
- <div ref="container">
3
+ <div ref="container" v-if="!isInputIgnored">
4
4
  <!-- A Stripe Element will be inserted here. -->
5
5
  </div>
6
6
  </div>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div>
2
+ <div v-if="!isInputIgnored">
3
3
  <div class="field">
4
4
  <div class="outlined">
5
5
  <div id="cardNumber" ref="cardNumber" class="input"></div>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()">
2
+ <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
3
3
  <v-text-field v-model="accountNumber" label="Account Number" placeholder="000123456" :variant="variant"
4
4
  :rules="accountNumberRules" :error-messages="errorMessages" @keyup="$onTyping({ duration: 200 })" />
5
5
 
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()">
2
+ <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
3
3
  <StripeIndividualFields v-if="$classes().includes('individual')" :spec="spec" :on-complete="_retrieveToken"
4
4
  :on-error="_displayError" />
5
5
  <StripeFields v-else :spec="spec" :on-complete="_retrieveToken" :on-error="_displayError" />
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()">
2
+ <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
3
3
  <v-text-field :color="gcolor" ref="field" v-model="fieldModel" :label="spec.label" :name="fieldName"
4
4
  :placeholder="spec.placeholder" :density="density" :hint="spec.hint" :maxlength="spec.maxLength || 255"
5
5
  :readonly="spec.readOnly" :disabled="inputDisabled" :type="config.type" :rules="rules"
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="styles()" :class="classes()">
2
+ <div :style="styles()" :class="classes()" v-if="!isInputIgnored">
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"
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="classes()">
2
+ <div :style="$styles()" :class="classes()" v-if="!isInputIgnored">
3
3
  <div v-if="days_mode">
4
4
  <div align="center">
5
5
  <v-row align="center" justify="center">
@@ -25,9 +25,15 @@ export default {
25
25
  var augmentedRules = rules || [];
26
26
  Utils.type.ifObject(this.spec.validation, val => {
27
27
  Utils.type.ifObject(val.required, spec => {
28
- augmentedRules = augmentedRules.concat([
29
- v => (v ? true : spec.message)
30
- ]);
28
+ const validationFunc = (v) => {
29
+ if (!Utils.type.isNotNull(v)) return spec.message;
30
+ if (Array.isArray(v) && v.length <= 0) return spec.message;
31
+ if (Utils.type.isString(v) && v.length <= 0) return spec.message;
32
+
33
+
34
+ return true;
35
+ };
36
+ augmentedRules = augmentedRules.concat([validationFunc]);
31
37
  });
32
38
  Utils.type.ifObject(val.format, spec => {
33
39
  augmentedRules = augmentedRules.concat([
@@ -3,6 +3,7 @@ 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 { vueApp } from "../../store";
6
7
 
7
8
  const NUMBER_PRECISION = 2;
8
9
  const isNeedToBeFixed = (val, component) => {
@@ -18,6 +19,7 @@ export default {
18
19
  _submitWhenNotDisplayed: !this.spec || !Utils.type.isNotNull(this.spec.submitWhenNotDisplayed) ? false : !!this.spec.submitWhenNotDisplayed,
19
20
  _watchers: [],
20
21
  _isValueChanged: false,
22
+ ignoredInputs: new Set(),
21
23
  // Some components do not support null or empty string value, so we need to use an intermediary value.
22
24
  // See https://github.com/vuetifyjs/vuetify/issues/8876
23
25
  vuetifyEmptyString: "<EMPTY_STRING>"
@@ -39,9 +41,25 @@ export default {
39
41
  gcolor() {
40
42
  if (!this.spec) return '';
41
43
  return determineColor(this.spec.styleClasses, this.spec.color || 'primary');
44
+ },
45
+ isInputIgnored() {
46
+ return vueApp.ignoredInputs.has(this.fieldName) && !this._submitWhenNotDisplayed;
42
47
  }
43
48
  },
44
49
  watch: {
50
+ _show: {
51
+ handler(val) {
52
+ this.ignoredInputs.forEach((input) => vueApp.ignoredInputs.delete(input));
53
+ this.ignoredInputs.clear();
54
+ if (!val && this.$el && this.$el instanceof HTMLElement && !this._submitWhenNotDisplayed) {
55
+ Array.from(this.$el.querySelectorAll('input,select,textarea')).forEach((el) => {
56
+ this.ignoredInputs.add(el.name);
57
+ vueApp.ignoredInputs.add(el.name);
58
+ });
59
+ }
60
+ },
61
+ // immediate: true
62
+ },
45
63
  fieldModel: function (val) {
46
64
  if (val === this.vuetifyEmptyString) {
47
65
  val = "";
@@ -114,7 +132,7 @@ export default {
114
132
  // this._watchers.push(watcher1);
115
133
  // }
116
134
 
117
- if (this.spec && (this.spec.showIf || this.spec.loadIf) && this.$jsonLogicEnabled()) {
135
+ if (this.spec && (this.spec.showIf || this.spec.loadIf)) {
118
136
  watcher2 = watchFieldModels(this.spec.showIf || this.spec.loadIf, (value) => {
119
137
  const oldValue = this._show;
120
138
 
@@ -124,6 +142,10 @@ export default {
124
142
  this._show = false;
125
143
  }
126
144
 
145
+ // if (this.spec.showIf) {
146
+ // this._submitWhenNotDisplayed = true;
147
+ // }
148
+
127
149
  if (oldValue != value) {
128
150
  this.$nextTick(() => {
129
151
  // Call either onIfTrue or onIfFalse.
@@ -266,12 +288,14 @@ export default {
266
288
  if (Utils.type.isNotNull(newSpec.displayed)) vm._show = newSpec.displayed;
267
289
  vm._submitWhenNotDisplayed = newSpec.submitWhenNotDisplayed;
268
290
 
269
- const els = vm.$el.querySelectorAll('input,textarea');
270
- if (!vm.submitWhenNotDisplayed) {
271
- els.forEach((el) => el.disabled = true);
272
- } else {
273
- els.forEach((el) => el.disabled = false);
274
- }
291
+ // if (vm.$el) {
292
+ // const els = vm.$el.querySelectorAll('input,textarea');
293
+ // if (!vm.submitWhenNotDisplayed) {
294
+ // els.forEach((el) => el.disabled = true);
295
+ // } else {
296
+ // els.forEach((el) => el.disabled = false);
297
+ // }
298
+ // }
275
299
 
276
300
  Object.assign(this.spec, newSpec);
277
301
  },
@@ -0,0 +1,13 @@
1
+ import { testPageUrl } from "../../helper"
2
+
3
+ describe('dialog', () => {
4
+ it('updateExisting', () => {
5
+ cy.visit(testPageUrl())
6
+ cy.contains('Dialog updateExisting').click()
7
+ cy.get('.v-dialog h1').should('contain.text', 'Hello world')
8
+ cy.get('.v-dialog .v-icon').should('exist')
9
+ cy.get('.v-dialog').contains('change dialog content').click()
10
+ cy.get('.v-dialog h1').should('contain.text', 'Hello world (updated)')
11
+ cy.get('.v-dialog .v-icon').should('not.exist')
12
+ })
13
+ })
@@ -0,0 +1,12 @@
1
+ import { testPageUrl } from "../../helper"
2
+
3
+ describe('display', () => {
4
+ it('form validity', () => {
5
+ cy.visit(testPageUrl())
6
+
7
+ cy.get('.fields-select .v-field__clearable').first().click()
8
+ cy.contains('submit (if form valid)').should('have.class', 'v-btn--disabled')
9
+ cy.contains('hide select').click()
10
+ cy.contains('submit (if form valid)').should('not.have.class', 'v-btn--disabled')
11
+ })
12
+ })
@@ -0,0 +1,48 @@
1
+ import { testPageUrl } from "../../helper"
2
+
3
+
4
+ describe('reactivity', () => {
5
+ it('logics/set, components/set, components/replace', () => {
6
+ cy.visit(testPageUrl())
7
+
8
+ // submit button should be clickabl e
9
+ cy.contains('submit (if form valid)').should('not.have.class', 'v-btn--disabled')
10
+
11
+ cy.contains('components/set').click()
12
+ cy.contains('logics/set').click()
13
+ cy.contains('components/replace').click()
14
+
15
+ // submit button should be disabled
16
+ cy.contains('submit (if form valid)').should('have.class', 'v-btn--disabled')
17
+
18
+ cy.get('.fields-check input[type="checkbox"]').first().click()
19
+
20
+ cy.contains('submit (if form valid)').should('have.class', 'v-btn--disabled')
21
+
22
+ cy.get('.fields-radio input[type="radio"]').first().click()
23
+
24
+ cy.contains('submit (if form valid)').should('have.class', 'v-btn--disabled')
25
+
26
+ cy.get('.fields-chipGroup .v-chip').first().click()
27
+
28
+ cy.contains('submit (if form valid)').should('not.have.class', 'v-btn--disabled')
29
+
30
+ cy.get('button').contains('submit').first().click()
31
+
32
+ const result = `Method: POST
33
+ Form Data:
34
+ {
35
+ "date": "2024-07-27",
36
+ "select": [
37
+ "option99"
38
+ ],
39
+ "chip_group": "option99",
40
+ "text": "Doe John",
41
+ "textarea": "The quick brown fox jumps over the lazy dog"
42
+ }`
43
+
44
+ cy.get('.unformatted').should('contain.text', result)
45
+ })
46
+
47
+
48
+ })
@@ -0,0 +1,7 @@
1
+ const DEV_TEST_PAGE = 'http://localhost:3000/glib/json_ui_garage?path=test_page%2Findex'
2
+
3
+ function testPageUrl() {
4
+ return DEV_TEST_PAGE;
5
+ }
6
+
7
+ export { testPageUrl }
package/nav/dialog.vue CHANGED
@@ -49,7 +49,7 @@ import { ref } from "vue";
49
49
  import { usePasteable } from "../components/composable/pasteable";
50
50
 
51
51
  export default {
52
- expose: ['isFormDirty', 'model'],
52
+ expose: ['isFormDirty', 'model', 'close'],
53
53
  setup(props) {
54
54
  const filePaster = ref(undefined);
55
55
  usePasteable(filePaster);
@@ -74,7 +74,6 @@ export default {
74
74
  model: null,
75
75
  url: null,
76
76
  urlLoaded: false,
77
- disableCloseButton: false,
78
77
  isMobile: false,
79
78
  formSpec: null,
80
79
  isFormDirty: false
@@ -101,10 +100,13 @@ export default {
101
100
  return `height: ${this.mainHeight}px;`;
102
101
  }
103
102
  return null;
103
+ },
104
+ disableCloseButton() {
105
+ return this.spec.disableCloseButton || Utils.type.isString(this.url);
104
106
  }
105
107
  },
106
108
  mounted() {
107
- dialogs.push(this);
109
+ dialogs.value.push(this);
108
110
  },
109
111
  created() {
110
112
  this.onResize();
@@ -135,7 +137,7 @@ export default {
135
137
  close() {
136
138
  if (!this.isDirty()) {
137
139
  this.filePaster = null;
138
- dialogs.remove(this);
140
+ dialogs.value.remove(this);
139
141
  this.model = false;
140
142
  }
141
143
  },
@@ -160,7 +162,6 @@ export default {
160
162
  show(reload) {
161
163
  const spec = this.spec;
162
164
  this.url ||= spec.url;
163
- this.disableCloseButton = this.spec.disableCloseButton || Utils.type.isString(this.url);
164
165
 
165
166
  if (Utils.type.isString(this.url)) {
166
167
  if (!this.urlLoaded) {
@@ -178,12 +179,10 @@ export default {
178
179
  this.urlLoaded = true;
179
180
  this.message = "";
180
181
  this.formSpec = response.fullPageForm;
181
- this.disableCloseButton = this.spec.disableCloseButton || false;
182
182
  this.updateContent(response);
183
183
  });
184
184
  },
185
185
  (error, response) => {
186
- this.disableCloseButton = false;
187
186
  Utils.launch.snackbar.error(error.toString(), this);
188
187
  }
189
188
  );
@@ -193,7 +192,6 @@ export default {
193
192
  // this.body = this.spec.body;
194
193
 
195
194
  this.message = this.spec.message;
196
- this.disableCloseButton = this.spec.disableCloseButton || false;
197
195
 
198
196
  this.updateContent(this.spec);
199
197
  }
@@ -205,6 +203,7 @@ export default {
205
203
  this.header = response.header;
206
204
  this.body = response.body;
207
205
  this.footer = response.footer;
206
+ this.spec.disableCloseButton = response.disableCloseButton;
208
207
 
209
208
  // Make sure action only executes after the content has finished populating.
210
209
  this.$nextTick(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glib-web",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/store.js CHANGED
@@ -1,4 +1,4 @@
1
- import { reactive } from 'vue';
1
+ import { reactive, ref } from 'vue';
2
2
  import { useDirtyState } from './components/composable/dirtyState';
3
3
  import Action from "./action";
4
4
 
@@ -18,6 +18,7 @@ export const vueApp = reactive({
18
18
  richTextValues: {},
19
19
  draggedComponent: null,
20
20
  lastNavigationCount: null,
21
+ ignoredInputs: new Set(),
21
22
  tooltipSpec: {},
22
23
  bottomBanners: {},
23
24
  uploader: {},
@@ -26,11 +27,14 @@ export const vueApp = reactive({
26
27
 
27
28
  export const jsonView = reactive({ page: window.__page });
28
29
 
29
- export const dialogs = reactive([]);
30
+ export const dialogs = ref([]);
30
31
 
31
- export const closeAllDialog = () => dialogs.forEach((dialog) => dialog.model = false);
32
+ export const closeAllDialog = () => {
33
+ dialogs.value.forEach((dialog) => dialog.model = false);
34
+ dialogs.value = [];
35
+ };
32
36
 
33
- export const ctx = () => dialogs.slice(-1)[0] || vueApp;
37
+ export const ctx = () => dialogs.value.slice(-1)[0] || vueApp;
34
38
 
35
39
  export const glibevent = reactive({
36
40
  onbeforewindowsopen: null,
@@ -39,12 +43,21 @@ export const glibevent = reactive({
39
43
  });
40
44
 
41
45
  function glibEventHandler(e) {
46
+ vueApp.ignoredInputs = new Set();
47
+
42
48
  const { onUnload } = jsonView.page;
43
49
  if (onUnload) {
44
50
  Action.execute(onUnload, {});
45
51
  }
46
52
 
47
- return !isDirty();
53
+ if (e) {
54
+ if (ctx().isFormDirty && !ctx().isFormSubmitted) {
55
+ e.preventDefault();
56
+ }
57
+ } else {
58
+ return !isDirty();
59
+ }
60
+
48
61
  }
49
62
  export const watchGlibEvent = () => {
50
63
  window.onbeforeunload = glibEventHandler;
package/utils/http.js CHANGED
@@ -81,7 +81,7 @@ export default class {
81
81
  const htmlUrl = Utils.url.htmlUrl(properties["url"]);
82
82
 
83
83
  Utils.http.execute(properties, "GET", component, (data, response) => {
84
- const pushHistory = windowMode ? true : !Utils.type.isObject(dialogs.last());
84
+ const pushHistory = windowMode ? true : !Utils.type.isObject(dialogs.value.last());
85
85
  // TODO: Check if it is okay to remove this `if` statement so we always push even if it's the same URL.
86
86
  if (forcePushHistory || (htmlUrl !== currentUrl && pushHistory)) {
87
87
  const redirectUrl = Utils.url.htmlUrl(response.url);
package/utils/launch.js CHANGED
@@ -93,7 +93,7 @@ class LaunchDialog {
93
93
 
94
94
 
95
95
  static topDialog() {
96
- return dialogs.last();
96
+ return dialogs.value.last();
97
97
  }
98
98
 
99
99
  static open(properties, component) {
@@ -122,13 +122,13 @@ class LaunchDialog {
122
122
  }
123
123
 
124
124
  static reload(properties, component) {
125
- Utils.type.ifObject(dialogs.last(), dialog => {
125
+ Utils.type.ifObject(dialogs.value.last(), dialog => {
126
126
  dialog.reload(properties);
127
127
  });
128
128
  }
129
129
 
130
130
  static close(properties, component) {
131
- Utils.type.ifObject(dialogs.last(), dialog => {
131
+ Utils.type.ifObject(dialogs.value.last(), dialog => {
132
132
  dialog.close();
133
133
  });
134
134
 
@@ -1,8 +0,0 @@
1
- describe('components set', () => {
2
- it('populate select options', () => {
3
- cy.visit('http://localhost:3000/glib/json_ui_garage?path=views%2Fcomponents_set')
4
- cy.get('.pages-body .v-btn').eq(0).click()
5
- cy.get('.fields-select').eq(0).click()
6
- cy.get('.v-overlay__content > .v-list').should('contain.text', 'Option1')
7
- })
8
- })
@@ -1,9 +0,0 @@
1
- describe('pick country and region', () => {
2
- it('autopopulate region', () => {
3
- cy.visit('http://localhost:3000/glib/json_ui_garage?path=forms%2Fpickers')
4
- cy.get('.fields-select').eq(0).click()
5
- cy.get('.v-overlay__content > .v-list').contains('Australia').click()
6
- cy.get('.fields-select').eq(1).click()
7
- cy.get('.v-overlay__content > .v-list').should('contain.text', 'ACT')
8
- })
9
- })
@@ -1,57 +0,0 @@
1
- describe('input select', () => {
2
- it('produce value', () => {
3
- const result = `Method: POST
4
- Form Data:
5
- {
6
- "city": "canberra",
7
- "cities": [
8
- "melbourne",
9
- "sydney"
10
- ],
11
- "language": "nl",
12
- "languages": [
13
- "ja",
14
- "de"
15
- ],
16
- "empty_default": "",
17
- "nil_default": "",
18
- "mixed_default": [
19
- "",
20
- "specified",
21
- ""
22
- ],
23
- "time_zone": "Asia/Jakarta"
24
- }`
25
-
26
- cy.visit('http://localhost:3000/glib/json_ui_garage?path=forms%2Fselects')
27
- cy.contains('Submit').click()
28
- cy.get('.unformatted').should('contain.text', result)
29
- })
30
-
31
- it('produce value (empty)', () => {
32
- const result = `Method: POST
33
- Form Data:
34
- {
35
- "city": "",
36
- "cities": [
37
- ""
38
- ],
39
- "language": "",
40
- "languages": [
41
- ""
42
- ],
43
- "empty_default": "",
44
- "nil_default": "",
45
- "mixed_default": [
46
- ""
47
- ],
48
- "time_zone": ""
49
- }`
50
-
51
- cy.visit('http://localhost:3000/glib/json_ui_garage?path=forms%2Fselects')
52
- cy.get('.v-field__clearable').click({ multiple: true })
53
- cy.contains('Submit').click()
54
-
55
- cy.get('.unformatted').should('contain.text', result)
56
- })
57
- })