@ouestfrance/sipa-bms-ui 7.14.1 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -12
- package/dist/components/form/BmsAutocomplete.vue.d.ts +8 -8
- package/dist/components/form/BmsInputText.vue.d.ts +13 -13
- package/dist/components/form/BmsSearch.vue.d.ts +13 -13
- package/dist/components/form/BmsSelect.vue.d.ts +9 -9
- package/dist/components/form/RawAutocomplete.vue.d.ts +30 -21
- package/dist/components/form/RawInputText.vue.d.ts +6 -6
- package/dist/components/navigation/UiTenantSwitcher.vue.d.ts +13 -13
- package/dist/components/table/BmsTableFilters.vue.d.ts +13 -13
- package/dist/mockServiceWorker.js +1 -1
- package/dist/models/form.model.d.ts +6 -0
- package/dist/plugins/field/FieldDatalist.spec.d.ts +1 -0
- package/dist/plugins/field/FieldDatalist.vue.d.ts +13 -12
- package/dist/sipa-bms-ui.css +81 -73
- package/dist/sipa-bms-ui.es.js +3405 -3315
- package/dist/sipa-bms-ui.es.js.map +1 -1
- package/dist/sipa-bms-ui.umd.js +3404 -3314
- package/dist/sipa-bms-ui.umd.js.map +1 -1
- package/package.json +13 -13
- package/src/components/form/BmsAutocomplete.vue +14 -8
- package/src/components/form/BmsSelect.spec.ts +5 -14
- package/src/components/form/BmsSelect.vue +8 -52
- package/src/components/form/RawAutocomplete.spec.ts +20 -17
- package/src/components/form/RawAutocomplete.vue +56 -53
- package/src/components/layout/BmsForm.stories.js +1 -1
- package/src/components/layout/BmsForm.vue +9 -5
- package/src/components/layout/BmsForm_retrocompat.stories.js +32 -0
- package/src/components/table/BmsTableFilters.vue +2 -2
- package/src/components/table/UiBmsTable.vue +4 -1
- package/src/models/form.model.ts +7 -0
- package/src/pages/Form.stories.js +37 -37
- package/src/plugins/field/FieldDatalist.spec.ts +35 -0
- package/src/plugins/field/FieldDatalist.stories.js +10 -0
- package/src/plugins/field/FieldDatalist.vue +85 -7
- package/src/showroom/pages/autocomplete.vue +7 -0
- package/src/showroom/pages/forms.vue +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouestfrance/sipa-bms-ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"author": "Ouest-France BMS",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"scripts": {
|
|
@@ -38,38 +38,38 @@
|
|
|
38
38
|
"@mdx-js/react": "3.1.0",
|
|
39
39
|
"@storybook/addon-actions": "^8.6.14",
|
|
40
40
|
"@storybook/addon-docs": "^9.0.4",
|
|
41
|
-
"@storybook/addon-links": "9.0.
|
|
42
|
-
"@storybook/vue3-vite": "9.0.
|
|
43
|
-
"@types/lodash": "4.17.
|
|
41
|
+
"@storybook/addon-links": "9.0.11",
|
|
42
|
+
"@storybook/vue3-vite": "9.0.11",
|
|
43
|
+
"@types/lodash": "4.17.18",
|
|
44
44
|
"@types/uuid": "10.0.0",
|
|
45
45
|
"@vitejs/plugin-vue": "5.2.4",
|
|
46
46
|
"@vue/test-utils": "2.4.6",
|
|
47
47
|
"@vueuse/core": "13.3.0",
|
|
48
48
|
"@vueuse/motion": "^3.0.0",
|
|
49
|
-
"axios": "1.
|
|
49
|
+
"axios": "1.10.0",
|
|
50
50
|
"blob-util": "^2.0.2",
|
|
51
|
-
"chromatic": "12.
|
|
51
|
+
"chromatic": "12.2.0",
|
|
52
52
|
"codemirror": "6.0.1",
|
|
53
53
|
"cors": "^2.8.5",
|
|
54
54
|
"cross-env": "^7.0.3",
|
|
55
55
|
"cy2": "4.0.9",
|
|
56
|
-
"cypress": "14.
|
|
56
|
+
"cypress": "14.5.0",
|
|
57
57
|
"express": "^5.0.0",
|
|
58
58
|
"husky": "9.1.7",
|
|
59
59
|
"jsdom": "26.1.0",
|
|
60
60
|
"keycloak-js": "26.1.2",
|
|
61
|
-
"lint-staged": "16.1.
|
|
61
|
+
"lint-staged": "16.1.2",
|
|
62
62
|
"lodash": "4.17.21",
|
|
63
|
-
"lucide-vue-next": "0.
|
|
63
|
+
"lucide-vue-next": "0.516.0",
|
|
64
64
|
"msw-storybook-addon": "^2.0.3",
|
|
65
65
|
"normalize.css": "8.0.1",
|
|
66
66
|
"path": "0.12.7",
|
|
67
67
|
"prettier": "3.5.3",
|
|
68
|
-
"sass": "1.89.
|
|
68
|
+
"sass": "1.89.2",
|
|
69
69
|
"semantic-release": "24.2.5",
|
|
70
70
|
"start-server-and-test": "2.0.12",
|
|
71
|
-
"storybook": "9.0.
|
|
72
|
-
"storybook-addon-pseudo-states": "9.0.
|
|
71
|
+
"storybook": "9.0.11",
|
|
72
|
+
"storybook-addon-pseudo-states": "9.0.11",
|
|
73
73
|
"storybook-vue3-router": "^5.0.0",
|
|
74
74
|
"typescript": "5.2.2",
|
|
75
75
|
"uuid": "11.1.0",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"vite-plugin-mkcert": "1.17.8",
|
|
79
79
|
"vite-plugin-pages": "0.33.0",
|
|
80
80
|
"vite-svg-loader": "5.1.0",
|
|
81
|
-
"vitest": "3.2.
|
|
81
|
+
"vitest": "3.2.4",
|
|
82
82
|
"vue": "3.5.16",
|
|
83
83
|
"vue-codemirror": "6.1.1",
|
|
84
84
|
"vue-loader": "17.4.2",
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
:required="required"
|
|
12
12
|
:helperText="helperText"
|
|
13
13
|
:placeholder="placeholder"
|
|
14
|
+
:can-add-new-option="canAddNewOption"
|
|
15
|
+
@select="(option: any) => emits('select', option)"
|
|
16
|
+
@add-new-option="(newOption: string) => emits('addNewOption', newOption)"
|
|
14
17
|
>
|
|
15
18
|
<template #icon-start v-if="currentOptionIcon">
|
|
16
19
|
<span
|
|
@@ -36,14 +39,12 @@
|
|
|
36
39
|
</template>
|
|
37
40
|
|
|
38
41
|
<script setup lang="ts">
|
|
39
|
-
import {
|
|
42
|
+
import { computed } from 'vue';
|
|
40
43
|
import RawAutocomplete from './RawAutocomplete.vue';
|
|
41
|
-
import { Caption } from '@/models';
|
|
44
|
+
import { Caption, InputOption } from '@/models';
|
|
42
45
|
|
|
43
46
|
export interface Props {
|
|
44
|
-
options:
|
|
45
|
-
| string[]
|
|
46
|
-
| { label: string; value: string; icon?: Component | string }[];
|
|
47
|
+
options: string[] | InputOption[];
|
|
47
48
|
modelValue?: string;
|
|
48
49
|
label?: string;
|
|
49
50
|
required?: boolean;
|
|
@@ -54,13 +55,18 @@ export interface Props {
|
|
|
54
55
|
captions?: string[] | Caption[];
|
|
55
56
|
errors?: string[] | Caption[];
|
|
56
57
|
open?: boolean;
|
|
58
|
+
canAddNewOption?: boolean;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
const props = withDefaults(defineProps<Props>(), {
|
|
60
62
|
open: false,
|
|
61
63
|
});
|
|
62
64
|
|
|
63
|
-
const modelValue = defineModel<string>('modelValue');
|
|
65
|
+
const modelValue = defineModel<string>('modelValue', { required: true });
|
|
66
|
+
const emits = defineEmits<{
|
|
67
|
+
addNewOption: [newOption: string];
|
|
68
|
+
select: [option: InputOption];
|
|
69
|
+
}>();
|
|
64
70
|
|
|
65
71
|
const currentOptionIcon = computed(() => {
|
|
66
72
|
const option = props.options.find(
|
|
@@ -74,8 +80,8 @@ const optionsLabelValue = computed(() =>
|
|
|
74
80
|
Array.isArray(props.options) &&
|
|
75
81
|
!!props.options.length &&
|
|
76
82
|
typeof props.options[0] === 'string'
|
|
77
|
-
? props.options.map((o) => ({ label: o, value: o }))
|
|
78
|
-
: props.options,
|
|
83
|
+
? props.options.map((o) => ({ label: o, value: o }) as InputOption)
|
|
84
|
+
: (props.options as InputOption[]),
|
|
79
85
|
);
|
|
80
86
|
</script>
|
|
81
87
|
|
|
@@ -3,7 +3,11 @@ import BmsSelect, { Props } from './BmsSelect.vue';
|
|
|
3
3
|
import { field } from '@/plugins/field';
|
|
4
4
|
import { nextTick } from 'vue';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
interface PropsAndModel extends Props {
|
|
7
|
+
modelValue: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const factory = (props: PropsAndModel) => {
|
|
7
11
|
const wrapper = mount(BmsSelect, { props, global: { plugins: [field] } });
|
|
8
12
|
const inputElement = wrapper.find('input');
|
|
9
13
|
const optionsElement = wrapper.find('ul');
|
|
@@ -33,19 +37,6 @@ describe('BmsSelect', () => {
|
|
|
33
37
|
expect(inputElement.element.value).toStrictEqual('toto-label');
|
|
34
38
|
});
|
|
35
39
|
|
|
36
|
-
it('should update when select with keys', async () => {
|
|
37
|
-
const { inputElement, optionsElement } = factory({
|
|
38
|
-
modelValue: '',
|
|
39
|
-
options: [{ label: 'toto-label', value: 'toto' }],
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
expect(inputElement.element.value).toStrictEqual('');
|
|
43
|
-
await inputElement.trigger('keydown.down');
|
|
44
|
-
await inputElement.trigger('keydown.enter');
|
|
45
|
-
await nextTick();
|
|
46
|
-
expect(inputElement.element.value).toStrictEqual('toto-label');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
40
|
it('should not display value if not in options', () => {
|
|
50
41
|
const { inputElement } = factory({
|
|
51
42
|
modelValue: 'toto',
|
|
@@ -13,13 +13,10 @@
|
|
|
13
13
|
ref="selectInput"
|
|
14
14
|
type="text"
|
|
15
15
|
role="input"
|
|
16
|
-
:value="
|
|
16
|
+
:value="displayValue"
|
|
17
17
|
:placeholder="placeholder"
|
|
18
18
|
onkeypress="return false;"
|
|
19
19
|
:required="required"
|
|
20
|
-
@keydown.up="keyUp"
|
|
21
|
-
@keydown.down="keyDown"
|
|
22
|
-
@keydown.enter="keyEnter"
|
|
23
20
|
/>
|
|
24
21
|
<span class="icon-down-container">
|
|
25
22
|
<ChevronDown class="icon-down-button" />
|
|
@@ -29,7 +26,7 @@
|
|
|
29
26
|
<FieldDatalist
|
|
30
27
|
v-show="isInputFocused"
|
|
31
28
|
:options="options"
|
|
32
|
-
:
|
|
29
|
+
:is-input-focused="isInputFocused"
|
|
33
30
|
@select="(option: any) => selectItem(option)"
|
|
34
31
|
/>
|
|
35
32
|
</template>
|
|
@@ -42,10 +39,10 @@ import { Ref, computed, onMounted, ref, watch } from 'vue';
|
|
|
42
39
|
import _ from 'lodash';
|
|
43
40
|
import FieldDatalist from '@/plugins/field/FieldDatalist.vue';
|
|
44
41
|
import { Caption } from '@/models/caption.model';
|
|
42
|
+
import { InputOption } from '@/models';
|
|
45
43
|
|
|
46
44
|
export interface Props {
|
|
47
|
-
|
|
48
|
-
options: { label: string; value: any }[];
|
|
45
|
+
options: InputOption[];
|
|
49
46
|
label?: string;
|
|
50
47
|
errors?: string[] | Caption[];
|
|
51
48
|
captions?: string[] | Caption[];
|
|
@@ -58,17 +55,14 @@ export interface Props {
|
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
const props = withDefaults(defineProps<Props>(), {
|
|
61
|
-
modelValue: '',
|
|
62
58
|
label: '',
|
|
63
59
|
disabled: false,
|
|
64
60
|
required: false,
|
|
65
61
|
open: false,
|
|
66
62
|
});
|
|
67
|
-
const
|
|
63
|
+
const modelValue = defineModel<any>('modelValue', { required: true });
|
|
68
64
|
|
|
69
65
|
const isInputFocused = ref<boolean>(props.open);
|
|
70
|
-
const inputValue = ref<any>(props.modelValue);
|
|
71
|
-
const currentSelectedItemIndex = ref<number>(-1);
|
|
72
66
|
const selectInput: Ref<HTMLElement | null> = ref(null);
|
|
73
67
|
const selectWrapper: Ref<HTMLElement | null> = ref(null);
|
|
74
68
|
|
|
@@ -87,57 +81,19 @@ onMounted(() => {
|
|
|
87
81
|
});
|
|
88
82
|
});
|
|
89
83
|
|
|
90
|
-
const
|
|
84
|
+
const displayValue = computed(() => {
|
|
91
85
|
const option = props.options.find((o) =>
|
|
92
|
-
_.isEqual(o.value,
|
|
86
|
+
_.isEqual(o.value, modelValue.value),
|
|
93
87
|
);
|
|
94
88
|
if (option) return option.label;
|
|
95
89
|
return '';
|
|
96
90
|
});
|
|
97
91
|
|
|
98
|
-
watch(inputValue, function emitUpdate(newValue) {
|
|
99
|
-
$emits('update:modelValue', newValue);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
watch(
|
|
103
|
-
() => props.modelValue,
|
|
104
|
-
function updateFromProps(newValue) {
|
|
105
|
-
inputValue.value = newValue;
|
|
106
|
-
},
|
|
107
|
-
);
|
|
108
|
-
|
|
109
92
|
const selectItem = (option: any) => {
|
|
110
|
-
|
|
93
|
+
modelValue.value = option.value;
|
|
111
94
|
isInputFocused.value = false;
|
|
112
95
|
};
|
|
113
96
|
|
|
114
|
-
const keyDown = () => {
|
|
115
|
-
if (!isInputFocused.value) isInputFocused.value = true;
|
|
116
|
-
if (currentSelectedItemIndex.value < props.options.length - 1) {
|
|
117
|
-
currentSelectedItemIndex.value++;
|
|
118
|
-
} else {
|
|
119
|
-
currentSelectedItemIndex.value = 0;
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const keyUp = () => {
|
|
124
|
-
if (!isInputFocused.value) isInputFocused.value = true;
|
|
125
|
-
if (currentSelectedItemIndex.value > 0) {
|
|
126
|
-
currentSelectedItemIndex.value--;
|
|
127
|
-
} else {
|
|
128
|
-
currentSelectedItemIndex.value = props.options.length - 1;
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const keyEnter = () => {
|
|
133
|
-
if (currentSelectedItemIndex.value >= 0) {
|
|
134
|
-
selectItem(props.options[currentSelectedItemIndex.value].value);
|
|
135
|
-
currentSelectedItemIndex.value = -1;
|
|
136
|
-
} else {
|
|
137
|
-
isInputFocused.value = false;
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
|
|
141
97
|
const setFocus = () => {
|
|
142
98
|
isInputFocused.value = true;
|
|
143
99
|
if (selectInput.value) {
|
|
@@ -5,7 +5,11 @@ import { mount } from '@vue/test-utils';
|
|
|
5
5
|
import { field } from '@/plugins/field';
|
|
6
6
|
import { nextTick } from 'vue';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
interface PropsAndModel extends Props {
|
|
9
|
+
modelValue: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const factory = (props?: PropsAndModel) => {
|
|
9
13
|
const wrapper = mount(RawAutocomplete, {
|
|
10
14
|
global: {
|
|
11
15
|
plugins: [field],
|
|
@@ -45,12 +49,14 @@ describe('BmsAutocomplete', () => {
|
|
|
45
49
|
});
|
|
46
50
|
|
|
47
51
|
it('should load with default value', async () => {
|
|
48
|
-
const { inputElement } = factory({ modelValue: 'i' } as
|
|
52
|
+
const { inputElement } = factory({ modelValue: 'i' } as PropsAndModel);
|
|
49
53
|
expect(inputElement.element.value).toStrictEqual('titi');
|
|
50
54
|
});
|
|
51
55
|
|
|
52
56
|
it('should change input value if parent modelvalue change', async () => {
|
|
53
|
-
const { wrapper, inputElement } = factory({
|
|
57
|
+
const { wrapper, inputElement } = factory({
|
|
58
|
+
modelValue: 'i',
|
|
59
|
+
} as PropsAndModel);
|
|
54
60
|
expect(inputElement.element.value).toStrictEqual('titi');
|
|
55
61
|
wrapper.setProps({ modelValue: 'u' });
|
|
56
62
|
await nextTick();
|
|
@@ -58,7 +64,9 @@ describe('BmsAutocomplete', () => {
|
|
|
58
64
|
});
|
|
59
65
|
|
|
60
66
|
it('should reset input value if parent modelvalue change for unknown value', async () => {
|
|
61
|
-
const { wrapper, inputElement } = factory({
|
|
67
|
+
const { wrapper, inputElement } = factory({
|
|
68
|
+
modelValue: 'i',
|
|
69
|
+
} as PropsAndModel);
|
|
62
70
|
expect(inputElement.element.value).toStrictEqual('titi');
|
|
63
71
|
wrapper.setProps({ modelValue: 'not_found' });
|
|
64
72
|
await nextTick();
|
|
@@ -66,29 +74,24 @@ describe('BmsAutocomplete', () => {
|
|
|
66
74
|
});
|
|
67
75
|
|
|
68
76
|
it('should not reset input value if parent modelvalue change for nullable value', async () => {
|
|
69
|
-
const { wrapper, inputElement } = factory({
|
|
77
|
+
const { wrapper, inputElement } = factory({
|
|
78
|
+
modelValue: 'i',
|
|
79
|
+
} as PropsAndModel);
|
|
70
80
|
expect(inputElement.element.value).toStrictEqual('titi');
|
|
71
81
|
wrapper.setProps({ modelValue: undefined });
|
|
72
82
|
await nextTick();
|
|
73
83
|
expect(inputElement.element.value).toStrictEqual('titi');
|
|
74
84
|
});
|
|
75
85
|
|
|
76
|
-
it('should
|
|
86
|
+
it('should emit event on option selection', async () => {
|
|
77
87
|
const { wrapper, inputElement } = factory();
|
|
78
88
|
const titi = wrapper.get('[data-testid="i"]');
|
|
79
89
|
expect(inputElement.element.value).toStrictEqual('');
|
|
80
90
|
await titi.trigger('click');
|
|
81
|
-
expect(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const { wrapper, inputElement } = factory();
|
|
86
|
-
const titi = wrapper.get('[data-testid="i"]');
|
|
87
|
-
|
|
88
|
-
expect(titi.classes()).toStrictEqual([]);
|
|
89
|
-
await inputElement.trigger('focus');
|
|
90
|
-
await inputElement.trigger('keydown.down');
|
|
91
|
-
expect(titi.classes()).toStrictEqual(['selected']);
|
|
91
|
+
expect(wrapper.emitted()['select']).toHaveLength(1);
|
|
92
|
+
expect(wrapper.emitted()['select'][0]).toStrictEqual([
|
|
93
|
+
{ value: 'i', label: 'titi' },
|
|
94
|
+
]);
|
|
92
95
|
});
|
|
93
96
|
|
|
94
97
|
it('should filter options', async () => {
|
|
@@ -13,9 +13,6 @@
|
|
|
13
13
|
v-model="inputText"
|
|
14
14
|
:type="InputType.TEXT"
|
|
15
15
|
:disabled="disabled"
|
|
16
|
-
@keyDown="keyDown"
|
|
17
|
-
@keyUp="keyUp"
|
|
18
|
-
@keyEnter="keyEnter"
|
|
19
16
|
:class="classes"
|
|
20
17
|
:placeholder="placeholder"
|
|
21
18
|
:required="required"
|
|
@@ -26,18 +23,31 @@
|
|
|
26
23
|
<slot name="icon-start"></slot>
|
|
27
24
|
</template>
|
|
28
25
|
<template #icon-end>
|
|
29
|
-
<slot name="icon-end"
|
|
26
|
+
<slot name="icon-end">
|
|
27
|
+
<span v-if="inputText.length" class="icon" @click="clearInput">
|
|
28
|
+
<X />
|
|
29
|
+
</span>
|
|
30
|
+
<span
|
|
31
|
+
v-else
|
|
32
|
+
class="icon"
|
|
33
|
+
@click.stop="isInputFocused = !isInputFocused"
|
|
34
|
+
>
|
|
35
|
+
<ChevronDown v-if="!isInputFocused" />
|
|
36
|
+
<ChevronUp v-else />
|
|
37
|
+
</span>
|
|
38
|
+
</slot>
|
|
30
39
|
</template>
|
|
31
40
|
</RawInputText>
|
|
32
41
|
<template #datalist>
|
|
33
42
|
<FieldDatalist
|
|
34
|
-
data-testid="autocomplete-menu"
|
|
35
43
|
v-show="isInputFocused"
|
|
44
|
+
data-testid="autocomplete-menu"
|
|
45
|
+
:is-input-focused="isInputFocused"
|
|
46
|
+
:can-add-new-option="canAddNewOption"
|
|
47
|
+
:new-option="inputText"
|
|
36
48
|
:options="filteredMenuItems"
|
|
37
|
-
|
|
38
|
-
@
|
|
39
|
-
(optionValue: string) => selectItem({ value: optionValue, label: '' })
|
|
40
|
-
"
|
|
49
|
+
@select="selectItem"
|
|
50
|
+
@add-new-option="(option: string) => emits('addNewOption', option)"
|
|
41
51
|
>
|
|
42
52
|
<template #option="{ option }: { option: any }">
|
|
43
53
|
<slot name="option" :option="option">{{ option }}</slot>
|
|
@@ -53,11 +63,11 @@ import FieldDatalist from '@/plugins/field/FieldDatalist.vue';
|
|
|
53
63
|
import { computed, ref, Ref, watch } from 'vue';
|
|
54
64
|
import { Caption } from '@/models/caption.model';
|
|
55
65
|
import RawInputText from '@/components/form/RawInputText.vue';
|
|
56
|
-
import { InputType } from '@/models';
|
|
66
|
+
import { InputOption, InputType } from '@/models';
|
|
67
|
+
import { ChevronDown, ChevronUp, X } from 'lucide-vue-next';
|
|
57
68
|
|
|
58
69
|
export interface Props {
|
|
59
|
-
options:
|
|
60
|
-
modelValue?: string | null;
|
|
70
|
+
options: InputOption[];
|
|
61
71
|
label?: string;
|
|
62
72
|
required?: boolean;
|
|
63
73
|
optional?: boolean;
|
|
@@ -67,6 +77,9 @@ export interface Props {
|
|
|
67
77
|
captions?: string[] | Caption[];
|
|
68
78
|
errors?: string[] | Caption[];
|
|
69
79
|
open?: boolean;
|
|
80
|
+
|
|
81
|
+
canAddNewOption?: boolean;
|
|
82
|
+
newOption?: string;
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -74,8 +87,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
74
87
|
open: false,
|
|
75
88
|
});
|
|
76
89
|
|
|
90
|
+
const modelValue = defineModel<string | null>('modelValue', { required: true });
|
|
91
|
+
|
|
77
92
|
const emits = defineEmits<{
|
|
78
|
-
|
|
93
|
+
addNewOption: [newOption: string];
|
|
94
|
+
select: [option: InputOption];
|
|
79
95
|
}>();
|
|
80
96
|
|
|
81
97
|
const getValidOptionByLabel = (label: string) =>
|
|
@@ -85,10 +101,9 @@ const getValidOptionByValue = (value: string | null) =>
|
|
|
85
101
|
props.options.find((o) => o.value === value);
|
|
86
102
|
|
|
87
103
|
const inputText = ref<string>(
|
|
88
|
-
getValidOptionByValue(
|
|
104
|
+
getValidOptionByValue(modelValue.value)?.label ?? '',
|
|
89
105
|
);
|
|
90
106
|
const isInputFocused = ref<boolean>(props.open);
|
|
91
|
-
const currentSelectedItemIndex = ref<number>(-1);
|
|
92
107
|
const autocompleteInput: Ref<HTMLElement | null> = ref(null);
|
|
93
108
|
|
|
94
109
|
const classes = computed(() => {
|
|
@@ -99,64 +114,42 @@ const filteredMenuItems = computed(() =>
|
|
|
99
114
|
props.options.filter((o) => searchString(o.label, inputText.value)),
|
|
100
115
|
);
|
|
101
116
|
|
|
102
|
-
const selectItem = (option:
|
|
117
|
+
const selectItem = (option: InputOption) => {
|
|
118
|
+
emits('select', option);
|
|
103
119
|
const existingOption =
|
|
104
120
|
getValidOptionByLabel(option.label) || getValidOptionByValue(option.value);
|
|
121
|
+
modelValue.value = existingOption?.value ?? null;
|
|
122
|
+
isInputFocused.value = false;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const displayItem = (option: InputOption) => {
|
|
126
|
+
const existingOption =
|
|
127
|
+
getValidOptionByLabel(option.label) || getValidOptionByValue(option.value);
|
|
128
|
+
|
|
105
129
|
if (existingOption) {
|
|
106
130
|
inputText.value = existingOption.label;
|
|
131
|
+
modelValue.value = existingOption.value;
|
|
107
132
|
} else {
|
|
133
|
+
modelValue.value = null;
|
|
108
134
|
inputText.value = option.label;
|
|
109
135
|
}
|
|
110
|
-
isInputFocused.value = false;
|
|
111
136
|
};
|
|
112
137
|
|
|
113
|
-
watch(inputText, function emitUpdate(newLabel) {
|
|
114
|
-
const option = getValidOptionByLabel(newLabel);
|
|
115
|
-
if (option) selectItem(option);
|
|
116
|
-
emits('update:modelValue', option?.value ?? null);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
138
|
watch(
|
|
120
|
-
() =>
|
|
139
|
+
() => modelValue.value,
|
|
121
140
|
function updateFromProps(newValue) {
|
|
122
141
|
const option = getValidOptionByValue(newValue);
|
|
123
142
|
if (option) {
|
|
124
|
-
|
|
143
|
+
displayItem(option);
|
|
144
|
+
if (modelValue.value !== option.value) selectItem(option);
|
|
125
145
|
// drop input value only if parent set nullable value
|
|
126
|
-
} else if (
|
|
146
|
+
} else if (modelValue.value !== undefined && modelValue.value !== null) {
|
|
127
147
|
inputText.value = '';
|
|
128
148
|
}
|
|
129
149
|
},
|
|
130
150
|
);
|
|
131
|
-
|
|
132
151
|
const onInput = () => {
|
|
133
152
|
isInputFocused.value = true;
|
|
134
|
-
currentSelectedItemIndex.value = -1;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const keyDown = () => {
|
|
138
|
-
if (currentSelectedItemIndex.value < filteredMenuItems.value.length - 1) {
|
|
139
|
-
currentSelectedItemIndex.value++;
|
|
140
|
-
} else {
|
|
141
|
-
currentSelectedItemIndex.value = 0;
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const keyUp = () => {
|
|
146
|
-
if (currentSelectedItemIndex.value > 0) {
|
|
147
|
-
currentSelectedItemIndex.value--;
|
|
148
|
-
} else {
|
|
149
|
-
currentSelectedItemIndex.value = props.options.length - 1;
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const keyEnter = () => {
|
|
154
|
-
if (currentSelectedItemIndex.value >= 0) {
|
|
155
|
-
selectItem(filteredMenuItems.value[currentSelectedItemIndex.value]);
|
|
156
|
-
currentSelectedItemIndex.value = -1;
|
|
157
|
-
} else {
|
|
158
|
-
isInputFocused.value = false;
|
|
159
|
-
}
|
|
160
153
|
};
|
|
161
154
|
|
|
162
155
|
const setFocus = () => {
|
|
@@ -164,6 +157,10 @@ const setFocus = () => {
|
|
|
164
157
|
autocompleteInput.value.focus();
|
|
165
158
|
}
|
|
166
159
|
};
|
|
160
|
+
const clearInput = () => {
|
|
161
|
+
inputText.value = '';
|
|
162
|
+
modelValue.value = null;
|
|
163
|
+
};
|
|
167
164
|
|
|
168
165
|
defineExpose({
|
|
169
166
|
setFocus,
|
|
@@ -215,4 +212,10 @@ defineExpose({
|
|
|
215
212
|
}
|
|
216
213
|
}
|
|
217
214
|
}
|
|
215
|
+
.icon {
|
|
216
|
+
height: 1em;
|
|
217
|
+
width: 1em;
|
|
218
|
+
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
}
|
|
218
221
|
</style>
|
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
<slot :name="name" />
|
|
6
6
|
</section>
|
|
7
7
|
</template>
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
</div>
|
|
12
|
-
</template>
|
|
8
|
+
<div class="actions">
|
|
9
|
+
<slot name="actions" v-if="hasSlotActions" />
|
|
10
|
+
</div>
|
|
13
11
|
</div>
|
|
14
12
|
</template>
|
|
15
13
|
|
|
@@ -34,8 +32,14 @@
|
|
|
34
32
|
}
|
|
35
33
|
</style>
|
|
36
34
|
<script lang="ts" setup>
|
|
35
|
+
import { computed } from 'vue';
|
|
36
|
+
|
|
37
37
|
const slots = defineSlots();
|
|
38
38
|
|
|
39
|
+
const hasSlotActions = computed((): boolean => {
|
|
40
|
+
return Object.keys(slots).includes('actions');
|
|
41
|
+
});
|
|
42
|
+
|
|
39
43
|
const slotIsAction = (slotName: string): boolean => {
|
|
40
44
|
return slotName === 'actions';
|
|
41
45
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import BmsForm from '@/components/layout/BmsForm.vue';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Composants/layout/BmsForm/for_retrocompat',
|
|
5
|
+
component: BmsForm,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const Template = (args) => ({
|
|
9
|
+
components: {
|
|
10
|
+
BmsForm,
|
|
11
|
+
},
|
|
12
|
+
setup() {
|
|
13
|
+
return { args };
|
|
14
|
+
},
|
|
15
|
+
template: `
|
|
16
|
+
<BmsForm v-bind="args">
|
|
17
|
+
<template #organization>
|
|
18
|
+
<p>custom slot you can define whith any name(organization)</p>
|
|
19
|
+
</template>
|
|
20
|
+
</BmsForm>
|
|
21
|
+
<BmsForm>
|
|
22
|
+
<template #site>
|
|
23
|
+
<p>custom slot you can define whith any name (site)</p>
|
|
24
|
+
</template>
|
|
25
|
+
<template #actions>
|
|
26
|
+
<div>Slot for Actions components</div>
|
|
27
|
+
</template>
|
|
28
|
+
</BmsForm>
|
|
29
|
+
`,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const Default = Template.bind({});
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
|
|
77
77
|
<script setup lang="ts">
|
|
78
78
|
import { Filter as FilterIcon, RefreshCcw, Save } from 'lucide-vue-next';
|
|
79
|
-
import { Filter, FilterType, SavedFilter } from '@/models';
|
|
79
|
+
import { Filter, FilterType, InputOption, SavedFilter } from '@/models';
|
|
80
80
|
import BmsSelect from '../form/BmsSelect.vue';
|
|
81
81
|
import BmsAutocomplete from '../form/BmsAutocomplete.vue';
|
|
82
82
|
import { useRoute } from 'vue-router';
|
|
@@ -155,7 +155,7 @@ const getFilterComponent = (type: FilterType) => {
|
|
|
155
155
|
|
|
156
156
|
const getFilterOptions = (
|
|
157
157
|
filter: Filter,
|
|
158
|
-
): undefined |
|
|
158
|
+
): undefined | InputOption[] | string[] => {
|
|
159
159
|
switch (filter.type) {
|
|
160
160
|
case 'boolean':
|
|
161
161
|
return [
|
|
@@ -347,7 +347,7 @@ onMounted(() => {
|
|
|
347
347
|
<template v-if="items.length">
|
|
348
348
|
<template v-for="item in items" :key="item">
|
|
349
349
|
<tr :class="{ selected: isItemSelected(item) }">
|
|
350
|
-
<td v-if="selectable">
|
|
350
|
+
<td v-if="selectable" class="bms-table__cell__checkbox">
|
|
351
351
|
<BmsTooltip
|
|
352
352
|
:direction="TooltipDirection.Right"
|
|
353
353
|
tooltip-text="Vous ne pouvez pas désélectionner un élément unitairement si vous avez choisi de sélectionner la totalité des éléments"
|
|
@@ -461,6 +461,9 @@ onMounted(() => {
|
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
&__cell {
|
|
464
|
+
&__checkbox {
|
|
465
|
+
width: 4em;
|
|
466
|
+
}
|
|
464
467
|
&--action {
|
|
465
468
|
display: flex;
|
|
466
469
|
justify-content: end;
|
package/src/models/form.model.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Caption } from '@/models/caption.model';
|
|
2
|
+
import { Component } from 'vue';
|
|
2
3
|
|
|
3
4
|
export interface BmsInputRadioGroupOption {
|
|
4
5
|
value: any;
|
|
@@ -28,3 +29,9 @@ export enum InputType {
|
|
|
28
29
|
DATE = 'date',
|
|
29
30
|
DATETIME = 'datetime-local',
|
|
30
31
|
}
|
|
32
|
+
|
|
33
|
+
export interface InputOption {
|
|
34
|
+
label: string;
|
|
35
|
+
value: any;
|
|
36
|
+
icon?: Component;
|
|
37
|
+
}
|