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.
- package/.nycrc.json +20 -5
- package/action.js +0 -6
- package/actions/logics/set.js +11 -0
- package/agent/glib_test_sync.yaml +108 -0
- package/components/_internal_button.vue +6 -5
- package/components/charts/series.js +17 -4
- package/components/component.vue +0 -1
- package/components/composable/form.js +34 -2
- package/components/fields/_select.vue +22 -32
- package/components/fields/radioGroup.vue +1 -1
- package/components/mixins/generic.js +1 -1
- package/components/mixins/updatableComponent.js +0 -1
- package/components/multimedia/video.vue +13 -1
- package/components/panels/flow.vue +23 -28
- package/components/panels/form.vue +0 -3
- package/cypress/component/component.cy.ts +423 -0
- package/cypress/e2e/glib-web/autoValidate.cy.ts +20 -1
- package/cypress/e2e/glib-web/browsers.cy.ts +55 -0
- package/cypress/e2e/glib-web/calendar.cy.ts +34 -0
- package/cypress/e2e/glib-web/calendarEmptyData.cy.ts +10 -0
- package/cypress/e2e/glib-web/carousel.cy.ts +53 -0
- package/cypress/e2e/glib-web/charts.cy.ts +11 -0
- package/cypress/e2e/glib-web/column.cy.ts +27 -0
- package/cypress/e2e/glib-web/commands.cy.ts +35 -0
- package/cypress/e2e/glib-web/components.cy.ts +50 -0
- package/cypress/e2e/glib-web/custom.cy.ts +17 -0
- package/cypress/e2e/glib-web/dialogOpen.cy.ts +14 -0
- package/cypress/e2e/glib-web/fileUploadNew.cy.ts +12 -0
- package/cypress/e2e/glib-web/flow.cy.ts +29 -0
- package/cypress/e2e/glib-web/form.cy.ts +1 -1
- package/cypress/e2e/glib-web/forms.cy.ts +46 -0
- package/cypress/e2e/glib-web/grid.cy.ts +27 -0
- package/cypress/e2e/glib-web/horizontal.cy.ts +39 -0
- package/cypress/e2e/glib-web/http.cy.ts +75 -0
- package/cypress/e2e/glib-web/image.cy.ts +53 -0
- package/cypress/e2e/glib-web/list.cy.ts +41 -0
- package/cypress/e2e/glib-web/listEditable.cy.ts +28 -0
- package/cypress/e2e/glib-web/listsAppend.cy.ts +27 -0
- package/cypress/e2e/glib-web/logicsSet.cy.ts +16 -0
- package/cypress/e2e/glib-web/multimediaVideo.cy.ts +43 -0
- package/cypress/e2e/glib-web/pagination.cy.ts +34 -0
- package/cypress/e2e/glib-web/panels.cy.ts +50 -0
- package/cypress/e2e/glib-web/popovers.cy.ts +34 -0
- package/cypress/e2e/glib-web/progressCircle.cy.ts +45 -0
- package/cypress/e2e/glib-web/responsive.cy.ts +21 -0
- package/cypress/e2e/glib-web/scroll.cy.ts +37 -0
- package/cypress/e2e/glib-web/sheets.cy.ts +94 -0
- package/cypress/e2e/glib-web/snackbars.cy.ts +51 -0
- package/cypress/e2e/glib-web/split.cy.ts +12 -0
- package/cypress/e2e/glib-web/storageItems.cy.ts +71 -0
- package/cypress/e2e/glib-web/table.cy.ts +28 -0
- package/cypress/e2e/glib-web/timeline.cy.ts +32 -0
- package/cypress/e2e/glib-web/timeouts.cy.ts +26 -0
- package/cypress/e2e/glib-web/ul.cy.ts +19 -0
- package/cypress/e2e/glib-web/vertical.cy.ts +41 -0
- package/cypress/e2e/glib-web/web.cy.ts +29 -0
- package/cypress/e2e/glib-web/windows.cy.ts +69 -0
- package/cypress/support/component-index.html +10 -0
- package/cypress/support/component.ts +11 -0
- package/cypress/support/launch-stub.js +8 -0
- package/cypress.config.ts +35 -0
- package/index.js +1 -5
- package/package.json +6 -3
- package/templates/editable.vue +47 -39
- package/actions/global_states/set.js +0 -13
- package/actions/global_states/watch.js +0 -19
- package/actions/right_banners/close.js +0 -7
- package/actions/right_banners/open.js +0 -25
- package/components/fields/richText.vue +0 -361
- package/components/fields/select.vue +0 -27
- package/components/mixins/extension.js +0 -9
- 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
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
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,
|
package/actions/logics/set.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
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 };
|
package/components/component.vue
CHANGED
|
@@ -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 (!
|
|
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
|
-
:
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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="
|
|
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 =
|
|
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
|
|
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
|
-
|
|
126
|
-
styles[
|
|
127
|
-
styles[
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
};
|