glib-web 4.42.0 → 4.42.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 (72) hide show
  1. package/.nycrc.json +20 -5
  2. package/action.js +0 -6
  3. package/actions/logics/set.js +11 -0
  4. package/agent/glib_test_sync.yaml +108 -0
  5. package/components/_internal_button.vue +6 -5
  6. package/components/charts/series.js +17 -4
  7. package/components/component.vue +0 -1
  8. package/components/composable/form.js +34 -2
  9. package/components/fields/_select.vue +22 -32
  10. package/components/fields/radioGroup.vue +1 -1
  11. package/components/mixins/generic.js +1 -1
  12. package/components/mixins/updatableComponent.js +0 -1
  13. package/components/multimedia/video.vue +13 -1
  14. package/components/panels/flow.vue +23 -28
  15. package/components/panels/form.vue +0 -3
  16. package/cypress/component/component.cy.ts +423 -0
  17. package/cypress/e2e/glib-web/autoValidate.cy.ts +20 -1
  18. package/cypress/e2e/glib-web/browsers.cy.ts +55 -0
  19. package/cypress/e2e/glib-web/calendar.cy.ts +34 -0
  20. package/cypress/e2e/glib-web/calendarEmptyData.cy.ts +10 -0
  21. package/cypress/e2e/glib-web/carousel.cy.ts +53 -0
  22. package/cypress/e2e/glib-web/charts.cy.ts +11 -0
  23. package/cypress/e2e/glib-web/column.cy.ts +27 -0
  24. package/cypress/e2e/glib-web/commands.cy.ts +35 -0
  25. package/cypress/e2e/glib-web/components.cy.ts +50 -0
  26. package/cypress/e2e/glib-web/custom.cy.ts +17 -0
  27. package/cypress/e2e/glib-web/dialogOpen.cy.ts +14 -0
  28. package/cypress/e2e/glib-web/fileUploadNew.cy.ts +12 -0
  29. package/cypress/e2e/glib-web/flow.cy.ts +29 -0
  30. package/cypress/e2e/glib-web/form.cy.ts +1 -1
  31. package/cypress/e2e/glib-web/forms.cy.ts +46 -0
  32. package/cypress/e2e/glib-web/grid.cy.ts +27 -0
  33. package/cypress/e2e/glib-web/horizontal.cy.ts +39 -0
  34. package/cypress/e2e/glib-web/http.cy.ts +75 -0
  35. package/cypress/e2e/glib-web/image.cy.ts +53 -0
  36. package/cypress/e2e/glib-web/list.cy.ts +41 -0
  37. package/cypress/e2e/glib-web/listEditable.cy.ts +28 -0
  38. package/cypress/e2e/glib-web/listsAppend.cy.ts +27 -0
  39. package/cypress/e2e/glib-web/logicsSet.cy.ts +16 -0
  40. package/cypress/e2e/glib-web/multimediaVideo.cy.ts +43 -0
  41. package/cypress/e2e/glib-web/pagination.cy.ts +34 -0
  42. package/cypress/e2e/glib-web/panels.cy.ts +50 -0
  43. package/cypress/e2e/glib-web/popovers.cy.ts +34 -0
  44. package/cypress/e2e/glib-web/progressCircle.cy.ts +45 -0
  45. package/cypress/e2e/glib-web/responsive.cy.ts +21 -0
  46. package/cypress/e2e/glib-web/scroll.cy.ts +37 -0
  47. package/cypress/e2e/glib-web/sheets.cy.ts +94 -0
  48. package/cypress/e2e/glib-web/snackbars.cy.ts +51 -0
  49. package/cypress/e2e/glib-web/split.cy.ts +12 -0
  50. package/cypress/e2e/glib-web/storageItems.cy.ts +71 -0
  51. package/cypress/e2e/glib-web/table.cy.ts +28 -0
  52. package/cypress/e2e/glib-web/timeline.cy.ts +32 -0
  53. package/cypress/e2e/glib-web/timeouts.cy.ts +26 -0
  54. package/cypress/e2e/glib-web/ul.cy.ts +19 -0
  55. package/cypress/e2e/glib-web/vertical.cy.ts +41 -0
  56. package/cypress/e2e/glib-web/web.cy.ts +29 -0
  57. package/cypress/e2e/glib-web/windows.cy.ts +69 -0
  58. package/cypress/support/component-index.html +10 -0
  59. package/cypress/support/component.ts +11 -0
  60. package/cypress/support/launch-stub.js +8 -0
  61. package/cypress.config.ts +35 -0
  62. package/index.js +1 -5
  63. package/package.json +6 -3
  64. package/templates/editable.vue +47 -39
  65. package/actions/global_states/set.js +0 -13
  66. package/actions/global_states/watch.js +0 -19
  67. package/actions/right_banners/close.js +0 -7
  68. package/actions/right_banners/open.js +0 -25
  69. package/components/fields/richText.vue +0 -361
  70. package/components/fields/select.vue +0 -27
  71. package/components/mixins/extension.js +0 -9
  72. package/components/panels/table2.vue +0 -113
package/.nycrc.json CHANGED
@@ -9,10 +9,25 @@
9
9
  ".ts",
10
10
  ".vue"
11
11
  ],
12
+ "exclude-after-remap": true,
13
+ "include": [
14
+ "action.js",
15
+ "actions/**",
16
+ "app.vue",
17
+ "components/**",
18
+ "constant.js",
19
+ "extensions/**",
20
+ "index.js",
21
+ "keys.js",
22
+ "nav/**",
23
+ "plugins/**",
24
+ "store.js",
25
+ "templates/**",
26
+ "utils/**"
27
+ ],
12
28
  "exclude": [
13
- "cypress/**",
14
- "coverage/**",
15
- "doc/**",
16
- "node_modules/**"
29
+ "actions/cables",
30
+ "actions/analytics",
31
+ "utils/private"
17
32
  ]
18
- }
33
+ }
package/action.js CHANGED
@@ -85,9 +85,6 @@ import ActionListsAppend from "./actions/lists/append";
85
85
  import ActionBottomBannersOpen from "./actions/bottom_banners/open";
86
86
  import ActionBottomBannersClose from "./actions/bottom_banners/close";
87
87
 
88
- import ActionGlobalStatesWatch from "./actions/global_states/watch";
89
- import ActionGlobalStatesSet from "./actions/global_states/set";
90
-
91
88
  import ActionDetectCountry from "./actions/browsers/detectCountry";
92
89
  import ActionDetectTimezone from "./actions/browsers/detectTimezone";
93
90
 
@@ -189,9 +186,6 @@ const actions = {
189
186
  "bottomBanners/open": ActionBottomBannersOpen,
190
187
  "bottomBanners/close": ActionBottomBannersClose,
191
188
 
192
- "globalStates/watch": ActionGlobalStatesWatch,
193
- "globalStates/set": ActionGlobalStatesSet,
194
-
195
189
  "files/upload": ActionFilesUpload,
196
190
 
197
191
  "browsers/detectCountry": ActionDetectCountry,
@@ -2,6 +2,7 @@ import jsonLogic from 'json-logic-js';
2
2
  import merge from 'lodash.merge';
3
3
  import { nextTick } from "vue";
4
4
  import { sanitize } from "../../components/composable/date";
5
+ import { htmlElement } from "../../components/helper";
5
6
  import { isPresent } from "../../utils/type";
6
7
  import { getFormData as _getFormData } from "../../components/composable/form";
7
8
 
@@ -94,6 +95,16 @@ export default class {
94
95
  if (!targetComponent) {
95
96
  console.warn("Component ID not found", id);
96
97
  }
98
+ Utils.type.ifObject(targetComponent, (component) => {
99
+ const element = htmlElement(component);
100
+ Utils.type.ifObject(element, (el) => {
101
+ Utils.type.ifFunction(el.closest, () => {
102
+ if (!el.closest('form')) {
103
+ console.warn("Target component is not inside a form", component.viewId || id);
104
+ }
105
+ });
106
+ });
107
+ });
97
108
  return targetComponent;
98
109
  }).filter((comp) => comp);
99
110
 
@@ -0,0 +1,108 @@
1
+ version: 1
2
+ name: glib-web-test-sync
3
+ description: >
4
+ Compare doc/garage/test_page pages with Cypress tests in cypress/e2e/glib-web and
5
+ scaffold missing tests when a page exists without coverage.
6
+
7
+ inputs:
8
+ doc_dir: doc/garage/test_page
9
+ test_dir: cypress/e2e/glib-web
10
+ test_helper: cypress/helper.ts
11
+ ai_test_prompt: |
12
+ Generate a Cypress e2e test for the glib test page: {{page_slug}}.
13
+ Inputs you can use:
14
+ - Source: {{doc_page_path}}
15
+ - Existing tests: {{existing_tests_dir}}
16
+ Requirements:
17
+ - Must use: import { testPageUrl } from "../../helper"
18
+ - Must set: const url = testPageUrl('{{page_slug}}')
19
+ - Click every button defined in {{doc_page_path}} (include nested buttons in dialogs/sheets/menus).
20
+ - For each button click, assert the expected result implied by its onClick action.
21
+ - Use one or more it blocks; revisit the page when state needs a reset.
22
+ - Match existing project patterns (describe/it, cy.visit, selectors, etc).
23
+ - Use ASCII only.
24
+ fallback_test_template: |
25
+ import { testPageUrl } from "../../helper"
26
+ const url = testPageUrl('{{page_slug}}')
27
+
28
+ const overlaySelectors = [
29
+ '.v-dialog',
30
+ '.v-snackbar',
31
+ '.v-menu__content',
32
+ '.v-sheet',
33
+ '.v-overlay__content',
34
+ ].join(',')
35
+
36
+ describe('{{page_slug}}', () => {
37
+ it('clicks each button', () => {
38
+ cy.visit(url)
39
+ cy.get('button').should('have.length.at.least', 1)
40
+ cy.get('button').then(($buttons) => {
41
+ const labels = Array.from($buttons)
42
+ .map((button) => button.innerText.trim())
43
+ .filter(Boolean)
44
+
45
+ cy.wrap(labels).each((label) => {
46
+ cy.visit(url)
47
+ cy.contains('button', label).click({ force: true })
48
+ cy.location('href').then((hrefAfter) => {
49
+ if (hrefAfter !== url) {
50
+ expect(hrefAfter).to.not.eq(url)
51
+ return
52
+ }
53
+
54
+ cy.get('body').then(($body) => {
55
+ if ($body.find(overlaySelectors).length) {
56
+ cy.get(overlaySelectors).should('exist')
57
+ } else {
58
+ cy.contains('button', label).should('exist')
59
+ }
60
+ })
61
+ })
62
+ })
63
+ })
64
+ })
65
+ })
66
+
67
+ constraints:
68
+ - Use utils/type.js for any type checks in helper scripts.
69
+ - Ignore files that begin with "_" under doc/garage/test_page.
70
+
71
+ steps:
72
+ - id: collect-doc-pages
73
+ instruction: |
74
+ List *.json.jbuilder files under {{inputs.doc_dir}}.
75
+ Remove the ".json.jbuilder" suffix to get page slugs.
76
+ Drop any file that starts with "_".
77
+ Output: doc_page_slugs.
78
+
79
+ - id: collect-test-page-slugs
80
+ instruction: |
81
+ Scan every *.cy.ts under {{inputs.test_dir}} and extract all occurrences of
82
+ testPageUrl('...'). Collect the string values into test_page_slugs.
83
+ If a file contains no testPageUrl calls, derive a fallback slug by:
84
+ - Removing the .cy.ts suffix.
85
+ - Converting camelCase to snake_case.
86
+ Output: test_page_slugs.
87
+
88
+ - id: diff-docs-vs-tests
89
+ instruction: |
90
+ Compute missing_page_slugs = doc_page_slugs - test_page_slugs.
91
+ Output: missing_page_slugs.
92
+
93
+ - id: generate-missing-test-content
94
+ instruction: |
95
+ For each slug in missing_page_slugs:
96
+ 1) Set doc_page_path = {{inputs.doc_dir}}/<slug>.json.jbuilder.
97
+ 2) Use the AI with {{inputs.ai_test_prompt}} to generate test content.
98
+ 3) If AI output is empty or invalid, use {{inputs.fallback_test_template}}.
99
+ Output: generated_tests_map (slug -> test_content).
100
+
101
+ - id: scaffold-missing-tests
102
+ instruction: |
103
+ For each slug in missing_page_slugs:
104
+ 1) Derive a filename by converting snake_case to camelCase and append ".cy.ts".
105
+ Example: file_upload_new -> fileUploadNew.cy.ts
106
+ 2) Create {{inputs.test_dir}}/<filename> if it does not already exist.
107
+ 3) Write generated_tests_map[<slug>] to the file.
108
+ Output: created_test_files.
@@ -82,8 +82,13 @@ export default {
82
82
  <style lang="scss">
83
83
  .v-btn {
84
84
  &.link {
85
+ /* `height: inherit/auto/initial` will stretch to max height when used inside a horizontal panel,
86
+ presumably because it is affected by the panel's flex arrangement. */
87
+ height: 16px;
88
+ font-size: 1rem;
89
+
85
90
  .v-icon {
86
- font-size: 16px;
91
+ font-size: 1.2rem;
87
92
  }
88
93
  }
89
94
  }
@@ -102,10 +107,6 @@ button {
102
107
  /* text-decoration: underline; */
103
108
  padding-left: 0;
104
109
  padding-right: 0;
105
- /* `height: inherit/auto/initial` will stretch to max height when used inside a horizontal panel,
106
- presumably because it is affected by the panel's flex arrangement. */
107
- height: 16px;
108
- font-size: 16px;
109
110
  line-height: 1;
110
111
  min-width: auto;
111
112
  box-shadow: none !important;
@@ -1,7 +1,9 @@
1
1
  import { Chart, Colors } from "chart.js";
2
2
  import chartDataLabels from 'chartjs-plugin-datalabels';
3
3
  import doughnutLabel from 'chartjs-plugin-doughnutlabel-v3';
4
- import { settings, Vue, vueApp } from "../..";
4
+ import { settings } from "../../utils/settings";
5
+ import { vueApp } from "../../store";
6
+ import * as TypeUtils from "../../utils/type";
5
7
  import { computePosition, flip, offset } from '@floating-ui/dom';
6
8
 
7
9
  import 'chartkick/chart.js';
@@ -15,8 +17,18 @@ Chart.register(Colors);
15
17
  if (settings.chartPlugin.htmlLegendPlugin) Chart.register(settings.chartPlugin.htmlLegendPlugin);
16
18
 
17
19
  import VueChartkick from 'vue-chartkick';
18
- import { computed } from "vue";
19
- Vue.use(VueChartkick);
20
+ import { computed, getCurrentInstance } from "vue";
21
+
22
+ let chartkickInstalled = false;
23
+ const installChartkick = () => {
24
+ if (chartkickInstalled) return;
25
+ const instance = getCurrentInstance();
26
+ const app = instance?.appContext?.app;
27
+ if (TypeUtils.isObject(app) && TypeUtils.isFunction(app.use)) {
28
+ app.use(VueChartkick);
29
+ chartkickInstalled = true;
30
+ }
31
+ };
20
32
 
21
33
  const multipleDataSeries = (dataSeries) => {
22
34
  return dataSeries.map((value) => {
@@ -110,6 +122,7 @@ const getData = (multiple, dataSeries, context) => {
110
122
  };
111
123
 
112
124
  function useChart({ dataSeries, spec, multiple = true }) {
125
+ installChartkick();
113
126
  const isDonut = [spec.styleClasses].flat().includes('donut');
114
127
  const { datalabels, centerLabel, customTooltip } = spec.plugins || {};
115
128
  const legend = spec.legend || { display: true };
@@ -198,4 +211,4 @@ function useChart({ dataSeries, spec, multiple = true }) {
198
211
  }
199
212
 
200
213
 
201
- export { useChart };
214
+ export { useChart };
@@ -68,7 +68,6 @@ import HiddenField from "./fields/hidden.vue";
68
68
  import TextField from "./fields/text.vue";
69
69
  import SubmitField from "./fields/submit.vue";
70
70
  import TextAreaField from "./fields/textarea.vue";
71
- const RichTextField = defineAsyncComponent(() => import("./fields/richText.vue"));
72
71
  const RichTextField2 = defineAsyncComponent(() => import("./fields/richText2.vue"));
73
72
  // import NewRichTextField from "./fields/newRichText.vue";
74
73
  const FileField = defineAsyncComponent(() => import("./fields/file.vue"));
@@ -1,5 +1,6 @@
1
1
  import { getCurrentInstance, inject, nextTick, onBeforeUnmount, onBeforeUpdate, onMounted, provide, ref, watch } from "vue";
2
2
  import { closest, htmlElement } from "../helper";
3
+ import { isArray, isFunction } from "../../utils/type.js";
3
4
 
4
5
  const setBusy = (htmlElement, value) => {
5
6
  const event = new Event('forms/setBusy', { bubbles: true });
@@ -13,6 +14,33 @@ const triggerOnChange = (htmlElement) => {
13
14
 
14
15
  const triggerOnInput = (htmlElement) => nextTick(() => htmlElement.dispatchEvent(new Event('input', { bubbles: true })));
15
16
 
17
+ const getCheckboxNames = (el) => {
18
+ const checkboxNames = new Set();
19
+ const nonCheckboxNames = new Set();
20
+ if (!el || !isFunction(el.querySelectorAll)) return checkboxNames;
21
+
22
+ el.querySelectorAll('[name]').forEach((input) => {
23
+ const name = input.name;
24
+ if (input instanceof HTMLInputElement && input.type === 'checkbox') {
25
+ if (!nonCheckboxNames.has(name)) checkboxNames.add(name);
26
+ return;
27
+ }
28
+ nonCheckboxNames.add(name);
29
+ checkboxNames.delete(name);
30
+ });
31
+
32
+ return checkboxNames;
33
+ };
34
+
35
+ const warnDuplicateNames = (key, seenNames, warnedNames, checkboxNames) => {
36
+ if (seenNames.has(key) && !warnedNames.has(key) && !checkboxNames.has(key)) {
37
+ console.warn(`Multiple inputs share the same name: "${key}".`);
38
+ warnedNames.add(key);
39
+ return;
40
+ }
41
+ seenNames.add(key);
42
+ };
43
+
16
44
  const getFormData = (el, ignoredFields = new Set()) => {
17
45
  if (!el) return {};
18
46
  if (!(el instanceof HTMLFormElement) && el instanceof HTMLElement) {
@@ -21,7 +49,11 @@ const getFormData = (el, ignoredFields = new Set()) => {
21
49
 
22
50
  const formData = new FormData(el);
23
51
  const obj = {};
52
+ const seenNames = new Set();
53
+ const warnedNames = new Set();
54
+ const checkboxNames = getCheckboxNames(el);
24
55
  formData.forEach((value, key) => {
56
+ warnDuplicateNames(key, seenNames, warnedNames, checkboxNames);
25
57
  if (ignoredFields.has(key)) return;
26
58
 
27
59
  // Reflect.has in favor of: object.hasOwnProperty(key)
@@ -29,7 +61,7 @@ const getFormData = (el, ignoredFields = new Set()) => {
29
61
  obj[key] = value;
30
62
  return;
31
63
  }
32
- if (!Array.isArray(obj[key])) {
64
+ if (!isArray(obj[key])) {
33
65
  obj[key] = [obj[key]];
34
66
  }
35
67
  obj[key].push(value);
@@ -118,4 +150,4 @@ function useGlibInput({ props, cacheValue = true }) {
118
150
 
119
151
  }
120
152
 
121
- export { setBusy, triggerOnChange, triggerOnInput, useGlibForm, useGlibInput, getFormData, getAllFormData };
153
+ export { setBusy, triggerOnChange, triggerOnInput, useGlibForm, useGlibInput, getFormData, getAllFormData };
@@ -3,11 +3,10 @@
3
3
  <!-- Set `menu-props` so the menu will never be wider than the select field.
4
4
  See https://github.com/vuetifyjs/vuetify/issues/17751 -->
5
5
  <component ref="comp" :is="compName" :color="gcolor" v-model="fieldModel" :label="label" :items="normalizedOptions"
6
- :disabled="inputDisabled" :multiple="spec.multiple" :readonly="spec.readOnly"
7
- :clearable="spec.clearable" :placeholder="spec.placeholder" :rules="$validation()" persistent-hint
8
- :append-icon="append.icon" validate-on="blur" item-title='text' :variant="variant"
9
- :density="density" persistent-placeholder @update:modelValue="onChange" @focus="focused = true"
10
- @blur="focused = false" :menu-props="{ maxWidth: 0 }">
6
+ :disabled="inputDisabled" :multiple="spec.multiple" :readonly="spec.readOnly" :clearable="spec.clearable"
7
+ :placeholder="spec.placeholder" :rules="$validation()" persistent-hint :append-icon="append.icon"
8
+ validate-on="input" item-title='text' :variant="variant" :density="density" persistent-placeholder
9
+ @update:modelValue="onChange" @focus="focused = true" @blur="focused = false" :menu-props="{ maxWidth: 0 }">
11
10
 
12
11
  <template #item="{ props, item }">
13
12
  <div class="fields-select-option">
@@ -23,7 +22,7 @@
23
22
  <select-item-default v-else :context="props" :item="item" :spec="spec"></select-item-default>
24
23
  </div>
25
24
  </template>
26
-
25
+
27
26
  <template v-slot:prepend-item>
28
27
  <template v-if="spec.prependSelectAll">
29
28
  <v-list-item title="Select All" @click="checkAll">
@@ -40,35 +39,18 @@
40
39
  </template>
41
40
 
42
41
  <template v-if="useChips" #selection="{ item, index }">
43
- <v-chip
44
- v-if="index < maxVisibleChips"
45
- :density="density"
46
- closable
47
- @click:close="removeItem(item)"
48
- >
42
+ <v-chip v-if="index < maxVisibleChips" :density="density" closable @click:close="removeItem(item)">
49
43
  <span>{{ item.title }}</span>
50
44
  </v-chip>
51
45
  <v-chip
52
46
  v-if="!expanded && chipExceedsTwoLines && visibleChipCount < fieldModel.length && index === visibleChipCount"
53
- :density="density"
54
- clickable
55
- @click="expanded = true"
56
- class="text-caption expansion-chip"
57
- variant="outlined"
58
- color="primary"
59
- >
47
+ :density="density" clickable @click="expanded = true" class="text-caption expansion-chip" variant="outlined"
48
+ color="primary">
60
49
  {{ fieldModel.length - visibleChipCount }} more
61
50
  <common-icon :spec="{ material: { name: 'expand_more' } }" class="ml-1" />
62
51
  </v-chip>
63
- <v-chip
64
- v-if="expanded && chipExceedsTwoLines && index === fieldModel.length - 1"
65
- :density="density"
66
- clickable
67
- @click="collapseChips"
68
- class="text-caption expansion-chip"
69
- variant="outlined"
70
- color="primary"
71
- >
52
+ <v-chip v-if="expanded && chipExceedsTwoLines && index === fieldModel.length - 1" :density="density" clickable
53
+ @click="collapseChips" class="text-caption expansion-chip" variant="outlined" color="primary">
72
54
  Show less
73
55
  <common-icon :spec="{ material: { name: 'expand_less' } }" class="ml-1" />
74
56
  </v-chip>
@@ -94,7 +76,7 @@
94
76
  import inputVariant from '../mixins/inputVariant';
95
77
  import { determineDensity } from "../../utils/constant";
96
78
  import { triggerOnChange, triggerOnInput, useGlibInput } from "../composable/form";
97
- import { isBoolean } from '../../utils/type';
79
+ import { isBoolean, isArray } from '../../utils/type';
98
80
 
99
81
  import { useGlibSelectable, watchNoneOfAbove } from '../composable/selectable';
100
82
  import { ref, defineExpose, computed, watch, nextTick, onMounted, onUnmounted } from 'vue';
@@ -402,9 +384,17 @@ export default {
402
384
  }
403
385
  },
404
386
  removeItem(item) {
405
- const index = this.fieldModel.indexOf(item.value);
406
- if (index >= 0) {
407
- this.fieldModel.splice(index, 1);
387
+ // fieldModel may not be an array when useChips is true but multiple is false.
388
+ // This can happen when spec.useChips is explicitly set to true while spec.multiple is false.
389
+ // In this scenario, fieldModel is a single value (not an array), so we need to handle both cases.
390
+ if (isArray(this.fieldModel)) {
391
+ const index = this.fieldModel.indexOf(item.value);
392
+ if (index >= 0) {
393
+ this.fieldModel.splice(index, 1);
394
+ }
395
+ } else {
396
+ // Single select mode - clear the value
397
+ this.fieldModel = null;
408
398
  }
409
399
  },
410
400
  $registryEnabled() {
@@ -4,7 +4,7 @@
4
4
  validation error on page load.
5
5
  -->
6
6
  <v-radio-group v-model="fieldModel" :name="fieldName" :readonly="spec.readOnly" :disabled="inputDisabled"
7
- :rules="$validation()" :inline="spec.row" validate-on="blur" @change="$executeOnChange()" :class="$classes()"
7
+ :rules="$validation()" :inline="spec.row" validate-on="input" @change="$executeOnChange()" :class="$classes()"
8
8
  :style="$styles()" v-if="loadIf">
9
9
  <!-- <div v-for="(childView, index) in spec.childViews" :key="index">
10
10
  <glib-component :spec="childView" />
@@ -43,7 +43,7 @@ export default {
43
43
  Utils.type.ifObject(this.spec.validation, val => {
44
44
  Object.keys(val).forEach((key) => {
45
45
  const validator = ValidationFactory.getValidator(key, val[key]);
46
- augmentedRules = augmentedRules.concat([validator.build.bind(validator)]);
46
+ augmentedRules = [validator.build.bind(validator), ...augmentedRules];
47
47
  });
48
48
  });
49
49
  return augmentedRules;
@@ -22,7 +22,6 @@ export default {
22
22
  if (spec && spec.id && this.$registryEnabled()) {
23
23
 
24
24
  const id = this.viewId;
25
-
26
25
  const existingComponent = GLib.component.findById(id);
27
26
  // A component with the same ID in a different page shouldn't be considered a
28
27
  // duplicate. See `utils/components#deregister` for more details.
@@ -24,12 +24,24 @@ export default {
24
24
  paddingBottom: null
25
25
  };
26
26
  },
27
+ watch: {
28
+ spec: {
29
+ deep: true,
30
+ handler() {
31
+ this.updateFromSpec();
32
+ }
33
+ }
34
+ },
27
35
  methods: {
28
36
  $ready() {
37
+ this.updateFromSpec();
38
+ },
39
+ updateFromSpec() {
40
+ this.paddingBottom = null;
29
41
  this.width = this.$length(this.spec.width);
30
42
  this.height = this.$length(this.spec.height);
31
43
 
32
- if (this.height == null) {
44
+ if (Utils.type.isNull(this.height)) {
33
45
  // Let the container calculates the height
34
46
  this.height = "100%";
35
47
 
@@ -122,35 +122,30 @@ export default {
122
122
  },
123
123
  methods: {
124
124
  applyStyles(styles, spec) {
125
- if (spec.gap) {
126
- styles['column-gap'] = `${spec.gap.y}px`;
127
- styles['row-gap'] = `${spec.gap.x}px`;
128
- if (spec.gap.all) {
129
- styles['gap'] = `${spec.gap.all}px`;
130
- }
131
- }
132
-
133
- if (spec.padding) {
125
+ Utils.type.ifObject(spec.gap, gap => {
126
+ Utils.type.ifNumber(gap.all, all => (styles["gap"] = `${all}px`));
127
+ Utils.type.ifNumber(gap.x, x => (styles["column-gap"] = `${x}px`));
128
+ Utils.type.ifNumber(gap.y, y => (styles["row-gap"] = `${y}px`));
129
+ });
134
130
 
135
- Utils.type.ifObject(spec.padding, padding => {
136
- Utils.type.ifNumber(
137
- padding.top || padding.y || padding.all,
138
- top => (styles["padding-top"] = `${top}px`)
139
- );
140
- Utils.type.ifNumber(
141
- padding.bottom || padding.y || padding.all,
142
- bottom => (styles["padding-bottom"] = `${bottom}px`)
143
- );
144
- Utils.type.ifNumber(
145
- padding.left || padding.x || padding.all,
146
- left => (styles["padding-left"] = `${left}px`)
147
- );
148
- Utils.type.ifNumber(
149
- padding.right || padding.x || padding.all,
150
- right => (styles["padding-right"] = `${right}px`)
151
- );
152
- });
153
- }
131
+ Utils.type.ifObject(spec.padding, padding => {
132
+ Utils.type.ifNumber(
133
+ padding.top || padding.y || padding.all,
134
+ top => (styles["padding-top"] = `${top}px`)
135
+ );
136
+ Utils.type.ifNumber(
137
+ padding.bottom || padding.y || padding.all,
138
+ bottom => (styles["padding-bottom"] = `${bottom}px`)
139
+ );
140
+ Utils.type.ifNumber(
141
+ padding.left || padding.x || padding.all,
142
+ left => (styles["padding-left"] = `${left}px`)
143
+ );
144
+ Utils.type.ifNumber(
145
+ padding.right || padding.x || padding.all,
146
+ right => (styles["padding-right"] = `${right}px`)
147
+ );
148
+ });
154
149
  },
155
150
  }
156
151
  };
@@ -135,9 +135,6 @@ export default {
135
135
  const onChange = this.spec.onChange || this.spec.onChangeAndLoad;
136
136
  this.formCtx = { form: this.$refs.form };
137
137
  const onChangeHandler = () => {
138
- if (this.$refs.form) {
139
- this.$refs.form.resetValidation();
140
- }
141
138
  this.formCtx = { form: this.$refs.form };
142
139
  if (onChange) this.$executeOnChange();
143
140
  };