@ouestfrance/sipa-bms-ui 8.24.1 → 8.24.3

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.24.1",
3
+ "version": "8.24.3",
4
4
  "author": "Ouest-France BMS",
5
5
  "license": "ISC",
6
6
  "scripts": {
@@ -36,16 +36,16 @@
36
36
  "@commitlint/cli": "20.3.1",
37
37
  "@commitlint/config-conventional": "20.3.1",
38
38
  "@mdx-js/react": "3.1.1",
39
- "@storybook/addon-docs": "10.1.11",
40
- "@storybook/addon-links": "10.1.11",
41
- "@storybook/vue3-vite": "10.1.11",
39
+ "@storybook/addon-docs": "10.2.0",
40
+ "@storybook/addon-links": "10.2.0",
41
+ "@storybook/vue3-vite": "10.2.0",
42
42
  "@types/lodash": "4.17.23",
43
43
  "@types/uuid": "11.0.0",
44
44
  "@vitejs/plugin-vue": "6.0.3",
45
45
  "@vue/test-utils": "2.4.6",
46
46
  "@vueuse/core": "13.9.0",
47
47
  "@vueuse/motion": "^3.0.0",
48
- "axios": "1.13.2",
48
+ "axios": "1.13.3",
49
49
  "blob-util": "^2.0.2",
50
50
  "chromatic": "13.3.5",
51
51
  "codemirror": "6.0.2",
@@ -58,19 +58,19 @@
58
58
  "jsdom": "27.4.0",
59
59
  "keycloak-js": "26.1.2",
60
60
  "lint-staged": "16.2.7",
61
- "lodash": "4.17.21",
62
- "lucide-vue-next": "0.562.0",
61
+ "lodash": "4.17.23",
62
+ "lucide-vue-next": "0.563.0",
63
63
  "msw": "^2.12.4",
64
64
  "msw-storybook-addon": "^2.0.3",
65
65
  "normalize.css": "8.0.1",
66
66
  "path": "0.12.7",
67
- "prettier": "3.8.0",
67
+ "prettier": "3.8.1",
68
68
  "remark-gfm": "^4.0.1",
69
- "sass": "1.97.2",
69
+ "sass": "1.97.3",
70
70
  "semantic-release": "25.0.2",
71
71
  "start-server-and-test": "2.1.3",
72
- "storybook": "10.1.11",
73
- "storybook-addon-pseudo-states": "10.1.11",
72
+ "storybook": "10.2.0",
73
+ "storybook-addon-pseudo-states": "10.2.0",
74
74
  "storybook-addon-tag-badges": "^3.0.2",
75
75
  "storybook-vue3-router": "^7.0.0",
76
76
  "typescript": "5.2.2",
@@ -81,11 +81,11 @@
81
81
  "vite-plugin-pages": "0.33.2",
82
82
  "vite-svg-loader": "5.1.0",
83
83
  "vitest": "3.2.4",
84
- "vue": "3.5.26",
84
+ "vue": "3.5.27",
85
85
  "vue-codemirror": "6.1.1",
86
86
  "vue-loader": "17.4.2",
87
87
  "vue-router": "4.6.4",
88
- "vue-tsc": "3.2.2"
88
+ "vue-tsc": "3.2.3"
89
89
  },
90
90
  "files": [
91
91
  "dist",
@@ -1,6 +1,7 @@
1
1
  <template>
2
2
  <RawAutocomplete
3
3
  v-model="modelValue"
4
+ ref="autocompleteElement"
4
5
  :options="optionsLabelValue"
5
6
  :open="open"
6
7
  :label="label"
@@ -42,7 +43,7 @@
42
43
  </template>
43
44
 
44
45
  <script setup lang="ts">
45
- import { computed } from 'vue';
46
+ import { computed, useTemplateRef } from 'vue';
46
47
  import RawAutocomplete from './RawAutocomplete.vue';
47
48
  import { InputOption } from '@/models';
48
49
  import { FieldComponentProps } from '@/plugins/field/field-component.model';
@@ -69,6 +70,8 @@ const emits = defineEmits<{
69
70
  input: [e: InputEvent];
70
71
  }>();
71
72
 
73
+ const autocompleteElement = useTemplateRef('autocompleteElement');
74
+
72
75
  const currentOptionIcon = computed(() => {
73
76
  const option = props.options.find(
74
77
  (o) => typeof o !== 'string' && o.value === modelValue.value,
@@ -84,6 +87,10 @@ const optionsLabelValue = computed(() =>
84
87
  ? props.options.map((o) => ({ label: o, value: o }) as InputOption)
85
88
  : (props.options as InputOption[]),
86
89
  );
90
+
91
+ defineExpose({
92
+ setFocus: () => autocompleteElement.value?.setFocus(),
93
+ });
87
94
  </script>
88
95
 
89
96
  <style scoped lang="scss">
@@ -39,8 +39,18 @@ const Template = (args) => ({
39
39
  export const Default = Template.bind({});
40
40
  Default.args = {
41
41
  title: 'Title',
42
- tabs: [{ name: 'Titi', id: 'titi' }, { name: 'Toto' }],
42
+ tabs: [
43
+ { name: 'Titi', id: 'titi' },
44
+ { name: 'Toto', id: 'toto' },
45
+ ],
43
46
  };
47
+
48
+ export const DefaultWithoutId = Template.bind({});
49
+ DefaultWithoutId.args = {
50
+ title: 'Title',
51
+ tabs: [{ name: 'Titi' }, { name: 'Toto' }],
52
+ };
53
+
44
54
  export const WithSelectedTabId = Template.bind({});
45
55
  WithSelectedTabId.args = {
46
56
  title: 'Title',
@@ -1,14 +1,22 @@
1
1
  <template>
2
2
  <UiTabs
3
3
  :title="title"
4
- :tabs="tabs"
5
- :initial-tab-id="selectedTabId"
6
- @click="$emits('click', $event)"
4
+ :tabs="computedTabs"
5
+ :selected-tab-id="selectedTabId"
6
+ @click="onClick"
7
7
  >
8
8
  <template v-if="needRouterEngine" #router="{ tab }">
9
- <router-link class="tab" :to="getTabTarget(tab)">{{
10
- tab.name
11
- }}</router-link>
9
+ <router-link
10
+ class="tab"
11
+ :to="getTabTarget(tab)"
12
+ :class="{
13
+ active: isTabSelected(tab, selectedTabId),
14
+ error: tab.error,
15
+ disabled: tab.disabled,
16
+ }"
17
+ @click="onClick(tab)"
18
+ >{{ tab.name }}</router-link
19
+ >
12
20
  </template>
13
21
  </UiTabs>
14
22
  </template>
@@ -16,9 +24,13 @@
16
24
  <script setup lang="ts">
17
25
  import { Tab } from '@/models/tab.model';
18
26
  import UiTabs from './UiTabs.vue';
19
- import { computed, ComputedRef, onMounted, ref, watch } from 'vue';
20
- import { RouteLocation, useRouter } from 'vue-router';
21
- import { getTabId } from '@/helpers/tab.helper';
27
+ import { computed, onMounted, ref, watch } from 'vue';
28
+ import { useRouter } from 'vue-router';
29
+ import {
30
+ getTabId,
31
+ isTabSelected,
32
+ isTabSelectedByRoute,
33
+ } from '@/helpers/tab.helper';
22
34
 
23
35
  const { currentRoute } = useRouter();
24
36
 
@@ -28,6 +40,8 @@ const props = defineProps<{
28
40
  initialTabId?: string;
29
41
  }>();
30
42
 
43
+ const selectedTabId = ref<string | null>(null);
44
+
31
45
  const getTabTarget = (tab: Tab) => {
32
46
  return tab.routePath ? { path: tab.routePath } : { name: tab.routeName };
33
47
  };
@@ -44,23 +58,38 @@ const needRouterEngine = computed(() => {
44
58
  );
45
59
  });
46
60
 
47
- const selectedTabId: ComputedRef<string | null> = computed(() => {
61
+ const onClick = (tab: Tab) => {
62
+ selectedTabId.value = tab.id;
63
+ $emits('click', tab);
64
+ };
65
+
66
+ const computedTabs = computed(() => {
67
+ return props.tabs.map((tab) => {
68
+ return { ...tab, id: getTabId(tab) }; // retro compatibility for id
69
+ });
70
+ });
71
+
72
+ const computedInitialTabId = () => {
48
73
  if (needRouterEngine.value) {
49
74
  const selectedTab =
50
- props.tabs.find((t) => isTabSelected(t, currentRoute.value)) || null;
51
- return selectedTab ? getTabId(selectedTab) : null;
75
+ props.tabs.find((t) => isTabSelectedByRoute(t, currentRoute.value)) ||
76
+ null;
77
+ selectedTabId.value = selectedTab ? selectedTab.id : null;
52
78
  } else if (props.initialTabId) {
53
- return props.initialTabId;
79
+ selectedTabId.value = props.initialTabId;
54
80
  } else {
55
- return null;
81
+ selectedTabId.value = null;
56
82
  }
83
+ };
84
+
85
+ onMounted(() => {
86
+ computedInitialTabId();
57
87
  });
58
88
 
59
- const isTabSelected = (tab: Tab, currentRoute: RouteLocation) =>
60
- tab.routePath
61
- ? currentRoute.path.includes(tab.routePath)
62
- : tab.routeName
63
- ? !!currentRoute.name &&
64
- (currentRoute.name as string).includes(tab.routeName)
65
- : false;
89
+ watch(
90
+ () => props.initialTabId,
91
+ () => {
92
+ computedInitialTabId();
93
+ },
94
+ );
66
95
  </script>
@@ -1,17 +1,24 @@
1
1
  <template>
2
- <span
3
- data-testid="tab"
4
- class="tab"
5
- :class="{ active: isTabSelected, error: tab.error, disabled: tab.disabled }"
6
- >
7
- <slot name="router" :tab="tab">{{ tab.name }}</slot>
8
- </span>
2
+ <slot name="router" :tab="tab">
3
+ <span
4
+ data-testid="tab"
5
+ class="tab"
6
+ :class="{
7
+ active: isActive,
8
+ error: tab.error,
9
+ disabled: tab.disabled,
10
+ }"
11
+ @click="emits('click', $event)"
12
+ >
13
+ {{ tab.name }}
14
+ </span>
15
+ </slot>
9
16
  </template>
10
17
 
11
18
  <script lang="ts" setup>
12
19
  import { Tab } from '@/models/tab.model';
13
20
  import { computed, ComputedRef } from 'vue';
14
- import { getTabId } from '@/helpers/tab.helper';
21
+ import { isTabSelected } from '@/helpers/tab.helper';
15
22
 
16
23
  const props = withDefaults(
17
24
  defineProps<{
@@ -21,14 +28,16 @@ const props = withDefaults(
21
28
  {},
22
29
  );
23
30
 
24
- const tabId = computed(() => getTabId(props.tab));
31
+ const emits = defineEmits<{
32
+ (e: 'click', event: MouseEvent): void;
33
+ }>();
25
34
 
26
- const isTabSelected: ComputedRef<boolean> = computed(
27
- () => (tabId.value && tabId.value === props.selectedTabId) || false,
35
+ const isActive: ComputedRef<boolean> = computed(() =>
36
+ isTabSelected(props.tab, props.selectedTabId),
28
37
  );
29
38
  </script>
30
39
 
31
- <style lang="scss" scoped>
40
+ <style lang="scss">
32
41
  .tab {
33
42
  --tab-border-color: transparent;
34
43
  --tab-color: var(--bms-font-color);
@@ -38,10 +47,10 @@ const isTabSelected: ComputedRef<boolean> = computed(
38
47
  border-bottom: 4px solid var(--tab-border-color);
39
48
  padding: 0 8px 16px 8px;
40
49
  cursor: pointer;
41
- :deep(a) {
42
- color: var(--tab-color);
43
- text-decoration: none;
44
- }
50
+ //:deep(a) {
51
+ // color: var(--tab-color);
52
+ // text-decoration: none;
53
+ //}
45
54
  &:hover,
46
55
  &__hover {
47
56
  --tab-border-color: var(--bms-main-50);
@@ -27,8 +27,12 @@ const Template = (args) => ({
27
27
  `,
28
28
  });
29
29
 
30
- export const Primary = Template.bind({});
31
- Primary.args = {
30
+ export const Default = Template.bind({});
31
+ Default.args = {
32
32
  title: 'Title',
33
- tabs: [{ name: 'Titi', id: 'titi' }, { name: 'Toto' }],
33
+ tabs: [
34
+ { name: 'Titi', id: 'titi' },
35
+ { name: 'Toto', id: 'toto' },
36
+ ],
37
+ selectedTabId: null,
34
38
  };
@@ -5,7 +5,7 @@
5
5
  </div>
6
6
  <UiTab
7
7
  v-for="tab in tabs"
8
- :key="getTabId(tab)"
8
+ :key="tab.id"
9
9
  :selectedTabId="selectedTabId"
10
10
  :tab="tab"
11
11
  @click="onTabClick(tab)"
@@ -18,36 +18,20 @@
18
18
  <script setup lang="ts">
19
19
  import { Tab } from '@/models/tab.model';
20
20
  import UiTab from './UiTab.vue';
21
- import { computed, ComputedRef, onMounted, ref, watch } from 'vue';
22
- import { getTabId } from '@/helpers/tab.helper';
23
21
 
24
22
  const props = defineProps<{
25
23
  title: string;
26
24
  tabs: Tab[];
27
- initialTabId?: string | null;
25
+ selectedTabId: string | null;
28
26
  }>();
29
27
 
30
- const selectedTabId = ref<string | null>(null);
31
-
32
- onMounted(() => {
33
- selectedTabId.value = props.initialTabId || null;
34
- });
35
-
36
- watch(
37
- () => props.initialTabId,
38
- () => {
39
- selectedTabId.value = props.initialTabId || null;
40
- },
41
- );
42
-
43
28
  const $emits = defineEmits<{
44
29
  (e: 'click', value: any): void;
45
30
  }>();
46
31
 
47
32
  const onTabClick = (tab: Tab) => {
48
- if (getTabId(tab) !== selectedTabId.value) {
33
+ if (tab.id !== props.selectedTabId) {
49
34
  $emits('click', tab);
50
- selectedTabId.value = getTabId(tab);
51
35
  }
52
36
  };
53
37
  </script>
@@ -0,0 +1,87 @@
1
+ import { isTabSelectedByRoute } from '@/helpers/tab.helper';
2
+ import { Tab } from '@/models/tab.model';
3
+ import { RouteLocation } from 'vue-router';
4
+ import { test } from 'vitest';
5
+
6
+ const createTab = (overrides: Partial<Tab> = {}): Tab => ({
7
+ id: 'tab-1',
8
+ name: 'Tab 1',
9
+ ...overrides,
10
+ });
11
+
12
+ const createRoute = (overrides: Partial<RouteLocation> = {}): RouteLocation =>
13
+ ({
14
+ path: '/default',
15
+ name: 'default',
16
+ ...overrides,
17
+ }) as RouteLocation;
18
+
19
+ describe('tab.helper', () => {
20
+ describe('isTabSelectedByRoute', () => {
21
+ describe('with routePath', () => {
22
+ test.each([
23
+ ['/users', '/users', true],
24
+ ['/users', '/users/123', true],
25
+ ['/users', '/admin', false],
26
+ ['/users', '/admin/users', true],
27
+ ])(
28
+ 'tab with routePath "%s" and currentRoute.path "%s" should return %s',
29
+ (routePath, path, expected) => {
30
+ const tab = createTab({ routePath });
31
+ const route = createRoute({ path });
32
+ expect(isTabSelectedByRoute(tab, route)).toBe(expected);
33
+ },
34
+ );
35
+ });
36
+
37
+ describe('with routeName (no routePath)', () => {
38
+ test.each([
39
+ ['users', 'users', true],
40
+ ['users', 'users-list', true],
41
+ ['users', 'admin', false],
42
+ ['users', 'admin-users', true],
43
+ ])(
44
+ 'tab with routeName "%s" and currentRoute.name "%s" should return %s',
45
+ (routeName, name, expected) => {
46
+ const tab = createTab({ routeName });
47
+ const route = createRoute({ name });
48
+ expect(isTabSelectedByRoute(tab, route)).toBe(expected);
49
+ },
50
+ );
51
+
52
+ test('should return false when currentRoute.name is undefined', () => {
53
+ const tab = createTab({ routeName: 'users' });
54
+ const route = createRoute({ name: undefined });
55
+ expect(isTabSelectedByRoute(tab, route)).toBe(false);
56
+ });
57
+
58
+ test('should return false when currentRoute.name is null', () => {
59
+ const tab = createTab({ routeName: 'users' });
60
+ const route = createRoute({ name: null as unknown as string });
61
+ expect(isTabSelectedByRoute(tab, route)).toBe(false);
62
+ });
63
+ });
64
+
65
+ describe('without routePath and routeName', () => {
66
+ test('should return false', () => {
67
+ const tab = createTab();
68
+ const route = createRoute({ path: '/users', name: 'users' });
69
+ expect(isTabSelectedByRoute(tab, route)).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe('routePath takes priority over routeName', () => {
74
+ test('should use routePath when both are defined', () => {
75
+ const tab = createTab({ routePath: '/admin', routeName: 'users' });
76
+ const route = createRoute({ path: '/admin', name: 'other' });
77
+ expect(isTabSelectedByRoute(tab, route)).toBe(true);
78
+ });
79
+
80
+ test('should not match routeName when routePath is defined but does not match', () => {
81
+ const tab = createTab({ routePath: '/admin', routeName: 'users' });
82
+ const route = createRoute({ path: '/other', name: 'users' });
83
+ expect(isTabSelectedByRoute(tab, route)).toBe(false);
84
+ });
85
+ });
86
+ });
87
+ });
@@ -1,3 +1,21 @@
1
1
  import { Tab } from '@/models';
2
+ import { sanitizeString } from '@/helpers/string.helper';
3
+ import { RouteLocation } from 'vue-router';
2
4
 
3
- export const getTabId = (tab: Tab) => tab.id || tab.name;
5
+ export const getTabId = (tab: Tab) => tab.id || sanitizeString(tab.name);
6
+
7
+ export const isTabSelectedByRoute = (
8
+ tab: Tab,
9
+ currentRoute: RouteLocation,
10
+ ): boolean =>
11
+ tab.routePath
12
+ ? currentRoute.path.includes(tab.routePath)
13
+ : tab.routeName
14
+ ? !!currentRoute.name &&
15
+ (currentRoute.name as string).includes(tab.routeName)
16
+ : false;
17
+
18
+ export const isTabSelected = (
19
+ tab: Tab,
20
+ selectedTabId: string | null,
21
+ ): boolean => (tab && tab.id === selectedTabId) || false;
@@ -1,7 +1,7 @@
1
1
  export interface Tab {
2
2
  routePath?: string;
3
3
  routeName?: string;
4
- id?: string;
4
+ id: string;
5
5
  name: string;
6
6
  disabled?: boolean;
7
7
  error?: boolean;
@@ -5,6 +5,7 @@
5
5
  <br />
6
6
  Valeur: {{ inputLabelValue }}
7
7
  <BmsAutocomplete
8
+ ref="autocompleteElement"
8
9
  label="Autocomplete avec label/value"
9
10
  :options="optionsLabelValue"
10
11
  v-model="inputLabelValue"
@@ -60,7 +61,7 @@
60
61
  import { BmsAutocomplete, BmsBackButton } from '@/index';
61
62
  import { Heart, Cat } from 'lucide-vue-next';
62
63
  import { range } from 'lodash';
63
- import { ref } from 'vue';
64
+ import { onMounted, ref, useTemplateRef } from 'vue';
64
65
  import BmsButton from '@/components/button/BmsButton.vue';
65
66
  import BmsSelect from '@/components/form/BmsSelect.vue';
66
67
  import BmsMultiSelect from '@/components/form/BmsMultiSelect.vue';
@@ -69,6 +70,11 @@ import BmsServerAutocomplete from '@/components/form/BmsServerAutocomplete.vue';
69
70
  const select = ref('');
70
71
  const multiSelect = ref(null);
71
72
  const multiSelectText = ref(null);
73
+ const autocompleteElement = useTemplateRef('autocompleteElement');
74
+
75
+ onMounted(() => {
76
+ (autocompleteElement.value as any)?.setFocus();
77
+ });
72
78
 
73
79
  const optionsLabelValue = ref(
74
80
  range(0, 30).map((i) =>