@ouestfrance/sipa-bms-ui 8.1.3 → 8.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouestfrance/sipa-bms-ui",
3
- "version": "8.1.3",
3
+ "version": "8.2.0",
4
4
  "author": "Ouest-France BMS",
5
5
  "license": "ISC",
6
6
  "scripts": {
@@ -37,18 +37,18 @@
37
37
  "@formkit/vue": "1.6.9",
38
38
  "@mdx-js/react": "3.1.0",
39
39
  "@storybook/addon-actions": "^8.6.14",
40
- "@storybook/addon-docs": "9.0.15",
41
- "@storybook/addon-links": "9.0.15",
42
- "@storybook/vue3-vite": "9.0.15",
40
+ "@storybook/addon-docs": "9.0.16",
41
+ "@storybook/addon-links": "9.0.16",
42
+ "@storybook/vue3-vite": "9.0.16",
43
43
  "@types/lodash": "4.17.20",
44
44
  "@types/uuid": "10.0.0",
45
45
  "@vitejs/plugin-vue": "6.0.0",
46
46
  "@vue/test-utils": "2.4.6",
47
- "@vueuse/core": "13.4.0",
47
+ "@vueuse/core": "13.5.0",
48
48
  "@vueuse/motion": "^3.0.0",
49
49
  "axios": "1.10.0",
50
50
  "blob-util": "^2.0.2",
51
- "chromatic": "13.0.1",
51
+ "chromatic": "13.1.2",
52
52
  "codemirror": "6.0.2",
53
53
  "cors": "^2.8.5",
54
54
  "cross-env": "^7.0.3",
@@ -68,12 +68,12 @@
68
68
  "sass": "1.89.2",
69
69
  "semantic-release": "24.2.6",
70
70
  "start-server-and-test": "2.0.12",
71
- "storybook": "9.0.15",
72
- "storybook-addon-pseudo-states": "9.0.15",
71
+ "storybook": "9.0.16",
72
+ "storybook-addon-pseudo-states": "9.0.16",
73
73
  "storybook-vue3-router": "^5.0.0",
74
74
  "typescript": "5.2.2",
75
75
  "uuid": "11.1.0",
76
- "vite": "7.0.0",
76
+ "vite": "7.0.3",
77
77
  "vite-plugin-dts": "^4.1.0",
78
78
  "vite-plugin-mkcert": "1.17.8",
79
79
  "vite-plugin-pages": "0.33.1",
@@ -83,7 +83,7 @@
83
83
  "vue-codemirror": "6.1.1",
84
84
  "vue-loader": "17.4.2",
85
85
  "vue-router": "4.5.1",
86
- "vue-tsc": "2.2.12"
86
+ "vue-tsc": "3.0.1"
87
87
  },
88
88
  "files": [
89
89
  "dist",
@@ -0,0 +1,82 @@
1
+ import BmsMultiSelect from '@/components/form/BmsMultiSelect.vue';
2
+
3
+ export default {
4
+ title: 'Composants/form/MultiSelect',
5
+ component: BmsMultiSelect,
6
+ };
7
+
8
+ const Template = (args) => ({
9
+ setup() {
10
+ return { args };
11
+ },
12
+ components: {
13
+ BmsMultiSelect,
14
+ },
15
+ template: `
16
+ <BmsMultiSelect v-bind="args"/>
17
+ `,
18
+ });
19
+
20
+ export const Default = Template.bind({});
21
+ Default.args = {
22
+ label: 'My label',
23
+ captions: ['Helping text'],
24
+ modelValue: [],
25
+ options: [
26
+ { label: 'optionA', value: 'a' },
27
+ { label: 'optionB', value: 'b' },
28
+ ],
29
+ };
30
+
31
+ export const WithValue = Template.bind({});
32
+ WithValue.args = {
33
+ label: 'My label',
34
+ modelValue: [{ label: 'optionA', value: 'a' }],
35
+ options: [
36
+ { label: 'optionA', value: 'a' },
37
+ { label: 'optionB', value: 'b' },
38
+ ],
39
+ };
40
+
41
+ export const Disabled = Template.bind({});
42
+ Disabled.args = {
43
+ label: 'My label',
44
+ modelValue: [{ label: 'optionA', value: 'a' }],
45
+ options: [
46
+ { label: 'optionA', value: 'a' },
47
+ { label: 'optionB', value: 'b' },
48
+ ],
49
+ disabled: true,
50
+ };
51
+
52
+ export const Opened = Template.bind({});
53
+ Opened.args = {
54
+ label: 'My label',
55
+ modelValue: [],
56
+ options: [
57
+ { label: 'optionA', value: 'a' },
58
+ { label: 'optionB', value: 'b' },
59
+ ],
60
+ open: true,
61
+ };
62
+
63
+ export const WithErrors = Template.bind({});
64
+ WithErrors.args = {
65
+ label: 'My label',
66
+ modelValue: [],
67
+ options: [
68
+ { label: 'optionA', value: 'a' },
69
+ { label: 'optionB', value: 'b' },
70
+ ],
71
+ errors: ['error 1', 'error 2'],
72
+ };
73
+
74
+ export const WithIncorrectValue = Template.bind({});
75
+ WithIncorrectValue.args = {
76
+ label: 'My label',
77
+ modelValue: [{ label: 'optionA', value: 'a' }],
78
+ options: [
79
+ { label: 'optionB', value: 'b' },
80
+ { label: 'optionC', value: 'c' },
81
+ ],
82
+ };
@@ -0,0 +1,140 @@
1
+ <template>
2
+ <BmsSelect
3
+ v-model:open="open"
4
+ :model-value="modelValue"
5
+ @update:model-value="onSelect"
6
+ :label="label"
7
+ :errors="errors"
8
+ :captions="captions"
9
+ :required="required"
10
+ :optional="optional"
11
+ :helperText="helperText"
12
+ :disabled="disabled"
13
+ :options="displayedOptions"
14
+ >
15
+ <template #input>
16
+ <div class="tags">
17
+ <BmsTag
18
+ v-for="tag in displayValues"
19
+ active
20
+ @dismiss="removeOption(tag.value)"
21
+ >
22
+ <component v-if="tag?.icon" :is="tag.icon" />
23
+ {{ tag.label }}
24
+ </BmsTag>
25
+ <input type="text" v-model="searching" class="search" />
26
+ </div>
27
+
28
+ <span class="icon-container">
29
+ <ChevronDown class="icon-button" v-if="modelValue.length === 0" />
30
+ <X v-else class="icon-button" @click="reset" />
31
+ </span>
32
+ </template>
33
+ <template #option="{ option }: { option: InputOption }">
34
+ <BmsTag active :can-be-dismissed="false">
35
+ <component v-if="option?.icon" :is="option.icon" />
36
+ {{ option.label }}
37
+ </BmsTag>
38
+ </template>
39
+ </BmsSelect>
40
+ </template>
41
+
42
+ <script lang="ts" setup>
43
+ import { computed, ref } from 'vue';
44
+ import { ChevronDown, X } from 'lucide-vue-next';
45
+ import BmsSelect from './BmsSelect.vue';
46
+ import BmsTag from './BmsTag.vue';
47
+ import { Caption, InputOption } from '@/models';
48
+ import _ from 'lodash';
49
+ import { searchString } from '@/helpers';
50
+
51
+ export interface Props {
52
+ options: InputOption[];
53
+ label?: string;
54
+ errors?: string[] | Caption[];
55
+ captions?: string[] | Caption[];
56
+ required?: boolean;
57
+ optional?: boolean;
58
+ disabled?: boolean;
59
+ helperText?: string;
60
+ placeholder?: string;
61
+ }
62
+
63
+ const props = withDefaults(defineProps<Props>(), {
64
+ label: '',
65
+ disabled: false,
66
+ required: false,
67
+ });
68
+
69
+ const searching = ref('');
70
+
71
+ const modelValue = defineModel<InputOption[]>('modelValue', {
72
+ required: true,
73
+ });
74
+ const open = defineModel<boolean>('open', { default: false });
75
+
76
+ const onSelect = (value: string) => {
77
+ const option = props.options.find((o) => o.value === value);
78
+ if (option) {
79
+ modelValue.value.push(option);
80
+ searching.value = '';
81
+ }
82
+ };
83
+
84
+ const removeOption = (value: string) => {
85
+ modelValue.value = modelValue.value.filter((o) => o.value !== value);
86
+ };
87
+
88
+ const reset = () => {
89
+ modelValue.value = [];
90
+ searching.value = '';
91
+ };
92
+
93
+ const displayedOptions = computed(() =>
94
+ props.options
95
+
96
+ .filter((o) => searchString(o.label, searching.value))
97
+ .filter(
98
+ (o) => !modelValue.value.find((option) => option.value === o.value),
99
+ ),
100
+ );
101
+
102
+ const displayValues = computed<InputOption[]>(() =>
103
+ modelValue.value.filter((o) =>
104
+ props.options.find((option) => option.value === o.value),
105
+ ),
106
+ );
107
+ </script>
108
+
109
+ <style lang="scss" scoped>
110
+ .tags {
111
+ display: flex;
112
+ gap: 0.5em;
113
+ padding: 10px 0;
114
+ flex-wrap: wrap;
115
+ width: 100%;
116
+
117
+ .search {
118
+ outline: none;
119
+ border: none;
120
+ background-color: transparent;
121
+ flex-grow: 1;
122
+ }
123
+ }
124
+ .icon {
125
+ &-container {
126
+ height: 100%;
127
+ display: flex;
128
+ align-items: center;
129
+
130
+ &:hover {
131
+ cursor: pointer;
132
+ }
133
+ }
134
+ &-button {
135
+ width: 1em;
136
+ margin: 0 1em 0 0.5em;
137
+ display: block;
138
+ }
139
+ }
140
+ </style>
@@ -9,26 +9,32 @@
9
9
  :disabled="disabled"
10
10
  >
11
11
  <span class="select-wrapper" ref="selectWrapper" :class="classes">
12
- <input
13
- ref="selectInput"
14
- type="text"
15
- role="input"
16
- :value="displayValue"
17
- :placeholder="placeholder"
18
- onkeypress="return false;"
19
- :required="required"
20
- />
21
- <span class="icon-down-container">
22
- <ChevronDown class="icon-down-button" />
23
- </span>
12
+ <slot name="input">
13
+ <input
14
+ ref="selectInput"
15
+ type="text"
16
+ role="input"
17
+ :value="displayValue"
18
+ :placeholder="placeholder"
19
+ onkeypress="return false;"
20
+ :required="required"
21
+ />
22
+ <span class="icon-down-container">
23
+ <ChevronDown class="icon-down-button" />
24
+ </span>
25
+ </slot>
24
26
  </span>
25
27
  <template #datalist>
26
28
  <FieldDatalist
27
- v-show="isInputFocused"
29
+ v-show="open"
28
30
  :options="options"
29
- :is-input-focused="isInputFocused"
31
+ :is-input-focused="open"
30
32
  @select="(option: any) => selectItem(option)"
31
- />
33
+ >
34
+ <template #option="{ option }: { option: any }">
35
+ <slot name="option" :option="option"></slot>
36
+ </template>
37
+ </FieldDatalist>
32
38
  </template>
33
39
  </field>
34
40
  </template>
@@ -51,18 +57,16 @@ export interface Props {
51
57
  disabled?: boolean;
52
58
  helperText?: string;
53
59
  placeholder?: string;
54
- open?: boolean;
55
60
  }
56
61
 
57
62
  const props = withDefaults(defineProps<Props>(), {
58
63
  label: '',
59
64
  disabled: false,
60
65
  required: false,
61
- open: false,
62
66
  });
63
67
  const modelValue = defineModel<any>('modelValue', { required: true });
68
+ const open = defineModel<boolean>('open', { default: false });
64
69
 
65
- const isInputFocused = ref<boolean>(props.open);
66
70
  const selectInput: Ref<HTMLElement | null> = ref(null);
67
71
  const selectWrapper: Ref<HTMLElement | null> = ref(null);
68
72
 
@@ -73,10 +77,10 @@ const classes = computed(() => {
73
77
  onMounted(() => {
74
78
  document.body.addEventListener('click', (ev) => {
75
79
  if ((ev.target as any).closest('.select-wrapper') === selectWrapper.value) {
76
- isInputFocused.value = !isInputFocused.value;
80
+ open.value = !open.value;
77
81
  ev.stopPropagation();
78
82
  } else {
79
- isInputFocused.value = false;
83
+ open.value = false;
80
84
  }
81
85
  });
82
86
  });
@@ -91,11 +95,11 @@ const displayValue = computed(() => {
91
95
 
92
96
  const selectItem = (option: any) => {
93
97
  modelValue.value = option.value;
94
- isInputFocused.value = false;
98
+ open.value = false;
95
99
  };
96
100
 
97
101
  const setFocus = () => {
98
- isInputFocused.value = true;
102
+ open.value = true;
99
103
  if (selectInput.value) {
100
104
  selectInput.value.focus();
101
105
  }
@@ -114,15 +118,16 @@ defineExpose({
114
118
 
115
119
  .select-wrapper {
116
120
  width: 100%;
117
- padding: 0;
121
+ padding: 0 0 0 1em;
118
122
  margin: 0.5em 0;
119
123
  border-radius: var(--bms-border-radius);
120
124
  border: 1px solid var(--field-border-color);
121
125
  background-color: var(--input-background-color);
122
- height: 48px;
126
+ min-height: 48px;
123
127
 
124
128
  display: flex;
125
129
  align-items: center;
130
+ justify-content: space-between;
126
131
 
127
132
  .icon-down {
128
133
  &-container {
@@ -167,8 +172,8 @@ defineExpose({
167
172
  cursor: pointer;
168
173
  outline: none;
169
174
  appearance: none;
170
- padding: 0 0 0 1em;
171
175
  margin: 0;
176
+ padding: 0;
172
177
  font: inherit;
173
178
  color: inherit;
174
179
  height: 24px;
@@ -1,5 +1,7 @@
1
1
  import BmsTable from '@/components/table/BmsTable.vue';
2
2
  import BmsButton from '@/components/button/BmsButton.vue';
3
+ import BmsIconButton from '@/components/button/BmsIconButton.vue';
4
+ import { Save, Trash } from 'lucide-vue-next';
3
5
 
4
6
  export default {
5
7
  title: 'Composants/table/Table',
@@ -928,3 +930,114 @@ WithCustomActions.args = {
928
930
  },
929
931
  ],
930
932
  };
933
+
934
+ const TemplateActionColumn = (args) => ({
935
+ components: {
936
+ BmsTable,
937
+ BmsIconButton,
938
+ Save,
939
+ Trash,
940
+ },
941
+ setup() {
942
+ return { args };
943
+ },
944
+ template: `
945
+ <BmsTable v-bind="args">
946
+ <template #col3="{isChildElement}">
947
+ <template v-if="isChildElement">
948
+ disabled
949
+ </template>
950
+ <template v-else>
951
+ <BmsIconButton><Save/></BmsIconButton>
952
+ <BmsIconButton mode="danger"><Trash/></BmsIconButton>
953
+ </template>
954
+ </template>
955
+ </BmsTable>
956
+ `,
957
+ });
958
+
959
+ export const WithActionColumn = TemplateActionColumn.bind({});
960
+ WithActionColumn.args = {
961
+ headers: [
962
+ {
963
+ label: 'Column 1',
964
+ key: 'col1',
965
+ align: 'start',
966
+ },
967
+ {
968
+ label: 'Column 2',
969
+ key: 'col2',
970
+ align: 'center',
971
+ },
972
+ {
973
+ label: 'Column actions',
974
+ key: 'col3',
975
+ action: true,
976
+ },
977
+ ],
978
+ items: [
979
+ {
980
+ col1: 'Value1',
981
+ col2: 'Value2',
982
+ col3: 'Value3',
983
+ },
984
+ {
985
+ col1: 'Value4',
986
+ col2: 'Value5',
987
+ col3: 'Value6',
988
+ },
989
+ ],
990
+ selectedItems: [
991
+ {
992
+ col1: 'Value1',
993
+ col2: 'Value2',
994
+ col3: 'Value3',
995
+ },
996
+ ],
997
+ selectable: true,
998
+ };
999
+
1000
+ export const WithActionColumnAndChildElement = TemplateActionColumn.bind({});
1001
+ WithActionColumnAndChildElement.args = {
1002
+ headers: [
1003
+ {
1004
+ label: 'Column 1',
1005
+ key: 'col1',
1006
+ align: 'start',
1007
+ },
1008
+ {
1009
+ label: 'Column 2',
1010
+ key: 'col2',
1011
+ align: 'center',
1012
+ },
1013
+ {
1014
+ label: 'Column actions',
1015
+ key: 'col3',
1016
+ action: true,
1017
+ },
1018
+ ],
1019
+ items: [
1020
+ {
1021
+ col1: 'Value1',
1022
+ col2: 'Value2',
1023
+ col3: 'Value3',
1024
+ },
1025
+ {
1026
+ col1: 'Value4',
1027
+ col2: 'Value5',
1028
+ col3: 'Value6',
1029
+ childElement: {
1030
+ col1: 'child1',
1031
+ col2: 'child2',
1032
+ col3: 'child3',
1033
+ },
1034
+ },
1035
+ {
1036
+ col1: 'Value10',
1037
+ col2: 'Value11',
1038
+ col3: 'Value12',
1039
+ },
1040
+ ],
1041
+ selectedItems: [],
1042
+ selectable: true,
1043
+ };
@@ -561,3 +561,44 @@ AllSelected.args = {
561
561
  selectable: true,
562
562
  totalSize: 2,
563
563
  };
564
+
565
+ export const WithChildElements = Template.bind({});
566
+ WithChildElements.args = {
567
+ headers: [
568
+ {
569
+ label: 'Column 1',
570
+ key: 'col1',
571
+ align: 'start',
572
+ },
573
+ {
574
+ label: 'Column 2',
575
+ key: 'col2',
576
+ align: 'center',
577
+ },
578
+ {
579
+ label: 'Column 3',
580
+ key: 'col3',
581
+ action: true,
582
+ // align: 'end',
583
+ },
584
+ ],
585
+ items: [
586
+ {
587
+ col1: 'Value1',
588
+ col2: 'Value2',
589
+ col3: 'Value3',
590
+ childElement: {
591
+ col1: 'Child1',
592
+ col2: 'Child2',
593
+ col3: 'Child3',
594
+ },
595
+ },
596
+ {
597
+ col1: 'Value1',
598
+ col2: 'Value2',
599
+ col3: 'Value3',
600
+ },
601
+ ],
602
+ hasFilters: true,
603
+ totalSize: 2,
604
+ };