@saooti/octopus-sdk 41.11.1 → 41.12.0-beta

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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 41.12.0 (En cours)
4
+
5
+ **Features**
6
+
7
+ - Export de `colorFromString` pour usage hors du sdk
8
+ - Ajout d'un composable `useSticky` pour déterminer quand un élément sticky se
9
+ déclenche
10
+ - Ajout de `@stylistic/eslint-plugin` et mise à jour des règles eslint
11
+ - Ajout d'un prop dans `ClassicPopover` pour modifier le z-index
12
+ - Ajout d'un slot `option` pour `ClassicSelect`
13
+ - Composants `Octopus`
14
+ - Ajout de `OctopusSelect`, une version moderne de `ClassicSelect`
15
+ - Mise en commun de code entre `OctopusSelect` & `OctopusMultiselect`
16
+ - Ajustement de `OctopusMultiselect` pour fonctionner correctement avec des
17
+ objets identiques avec références différentes
18
+ - Ajout de nouvelles classes utilitaires dans le css
19
+ - Ajout de nouvelles options d'apparence pour `ClassicButton`
20
+ - Changement propriété principale de `ClassicButtonGroup` pour plus de cohérence
21
+
22
+ **Fixes**
23
+
24
+ - Correction d'une exception dans `deepEquals` si la comparaison est faite entre
25
+ une variable primitive et un objet
26
+
27
+ **Misc**
28
+
29
+ - Mise à jour des règles eslint
30
+
3
31
  ## 41.11.1 (17/06/2026)
4
32
 
5
33
  **Fixes**
package/eslint-config.mjs CHANGED
@@ -2,6 +2,7 @@ import eslint from '@eslint/js';
2
2
  import eslintPluginVue from 'eslint-plugin-vue';
3
3
  import globals from 'globals';
4
4
  import typescriptEslint from 'typescript-eslint';
5
+ import stylistic from '@stylistic/eslint-plugin';
5
6
 
6
7
  export default typescriptEslint.config(
7
8
  { ignores: ['*.d.ts', '**/coverage', '**/dist'] },
@@ -11,6 +12,9 @@ export default typescriptEslint.config(
11
12
  ...typescriptEslint.configs.recommended,
12
13
  ...eslintPluginVue.configs['flat/recommended'],
13
14
  ],
15
+ plugins: {
16
+ '@stylistic': stylistic,
17
+ },
14
18
  files: ['**/*.{ts,vue}'],
15
19
  languageOptions: {
16
20
  ecmaVersion: 'latest',
@@ -33,11 +37,15 @@ export default typescriptEslint.config(
33
37
  "vue/no-ref-as-operand": ['error'],
34
38
 
35
39
  // Indentation
40
+ "@stylistic/indent": ['warn', 4, { "SwitchCase": 0 }],
36
41
  "vue/html-indent": ['warn', 4],
37
42
  "vue/script-indent": ['warn', 4],
43
+ "@stylistic/no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
38
44
 
39
45
  // Number of attributes per line (increase because sometimes two is not a lot)
40
- "vue/max-attributes-per-line": ['warn', { singleline: 2 } ]
41
- },
46
+ "vue/max-attributes-per-line": ['warn', { singleline: 2 } ],
47
+
48
+ "@typescript-eslint/no-unused-vars": "warn"
49
+ }
42
50
  }
43
51
  );
package/index.ts CHANGED
@@ -59,7 +59,8 @@ export * from "./src/components/buttons/";
59
59
  // Form
60
60
  import ClassicButtonGroup from "./src/components/form/ClassicButtonGroup.vue";
61
61
  import OctopusMultiselect from "./src/components/form/OctopusMultiselect.vue";
62
- export { ClassicButtonGroup, OctopusMultiselect };
62
+ import OctopusSelect from "./src/components/form/OctopusSelect.vue";
63
+ export { ClassicButtonGroup, OctopusMultiselect, OctopusSelect };
63
64
  export type { ButtonGroupOption } from "./src/components/form/ClassicButtonGroup.vue";
64
65
 
65
66
  //Display
@@ -144,6 +145,7 @@ export { useSeoTitleUrl } from "./src/components/composable/route/useSeoTitleUrl
144
145
  export { useSeasonsManagement } from "./src/components/composable/useSeasonsManagement.ts";
145
146
  export { useTranslation } from "./src/components/composable/useTranslation.ts";
146
147
  export { useDayjs } from "./src/components/composable/useDayjs.ts";
148
+ export { useSticky } from "./src/components/composable/useSticky.ts";
147
149
 
148
150
  //helper
149
151
  import domHelper from "./src/helper/domHelper.ts";
@@ -155,6 +157,7 @@ import downloadHelper from "./src/helper/downloadHelper.ts";
155
157
  import displayHelper from "./src/helper/displayHelper.ts";
156
158
  import debounce from "./src/helper/debounceHelper.ts";
157
159
  import { deepEqual } from "./src/helper/equals.ts";
160
+ export { colorFromString } from './src/helper/colorFromString';
158
161
 
159
162
  //stores
160
163
  import {useVastStore} from "./src/stores/VastStore.ts";
@@ -175,7 +178,7 @@ export * from "./src/api";
175
178
  // Types
176
179
  export { type Emission, SeasonMode, emptyEmissionData } from "./src/stores/class/general/emission.ts";
177
180
  export { type Organisation, type OrganisationAttributes, emptyOrganisationData, emptyOrgaData } from "./src/stores/class/general/organisation.ts";
178
- export { type Podcast, type PodcastAvailability, PodcastType } from "./src/stores/class/general/podcast.ts";
181
+ export { type Podcast, type SimplifiedPodcast, type PodcastAvailability, PodcastType, podcastToSimplified } from "./src/stores/class/general/podcast.ts";
179
182
  export { type Playlist, type PlaylistRule } from "./src/stores/class/general/playlist.ts";
180
183
  export { type Annotations } from "./src/stores/class/general";
181
184
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saooti/octopus-sdk",
3
- "version": "41.11.1",
3
+ "version": "41.12.0-beta",
4
4
  "private": false,
5
5
  "description": "Javascript SDK for using octopus",
6
6
  "author": "Saooti",
@@ -61,6 +61,7 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "@pinia/testing": "^1.0.3",
64
+ "@stylistic/eslint-plugin": "*",
64
65
  "@types/sockjs-client": "^1.5.4",
65
66
  "@types/webpack-env": "^1.18.8",
66
67
  "@vitejs/plugin-vue": "^5.2.4",
@@ -90,9 +91,11 @@
90
91
  "url": "git+https://github.com/saooti/octopus-sdk.git"
91
92
  },
92
93
  "peerDependencies": {
94
+ "@stylistic/eslint-plugin": "^5.10.0",
93
95
  "dayjs": "^1.11.0",
94
96
  "eslint-plugin-vue": "^10.9.1",
95
97
  "pinia": ">=2.3.0",
98
+ "typescript-eslint": "^8.47.0",
96
99
  "vue": "^3.5.0",
97
100
  "vue-i18n": "^11.1.0",
98
101
  "vue-router": "^4.6.0"
@@ -14,6 +14,19 @@ async function get(radioId: number): Promise<Canal> {
14
14
  });
15
15
  }
16
16
 
17
+ /**
18
+ * Retrieve all radio canal by organisation
19
+ * @param organisationId Id of the organisation
20
+ * @returns The list of canal data
21
+ */
22
+ async function getAll(organisationId: string): Promise<Array<Canal>> {
23
+ return await classicApi.fetchData<Array<Canal>>({
24
+ api: ModuleApi.RADIO,
25
+ path: "canal/orga/" + organisationId + "/"
26
+ });
27
+ }
28
+
17
29
  export const radioApi = {
18
- get
30
+ get,
31
+ getAll
19
32
  };
@@ -1,7 +1,7 @@
1
1
  <!-- A simple button with accessibility in mind -->
2
2
  <template>
3
3
  <button
4
- class="btn"
4
+ :class="classes"
5
5
  :aria-disabled="disabled"
6
6
  :title="title"
7
7
  :aria-label="title"
@@ -12,11 +12,17 @@
12
12
  </template>
13
13
 
14
14
  <script setup lang="ts">
15
+ import { computed } from 'vue';
16
+
15
17
  const props = defineProps<{
16
18
  /** Disable the button */
17
19
  disabled?: boolean;
18
20
  /** Title of the button */
19
21
  title?: string;
22
+ /** Primary style for button */
23
+ primary?: boolean;
24
+ /** Icon style for button */
25
+ icon?: boolean;
20
26
  }>();
21
27
 
22
28
  const emit = defineEmits<{
@@ -24,6 +30,17 @@ const emit = defineEmits<{
24
30
  (e: 'click'): void;
25
31
  }>();
26
32
 
33
+ const classes = computed((): Array<string> => {
34
+ const ary: Array<string> = ['btn', 'd-flex'];
35
+ if (props.primary) {
36
+ ary.push('btn-primary');
37
+ }
38
+ if (props.icon) {
39
+ ary.push('btn-icon');
40
+ }
41
+ return ary;
42
+ });
43
+
27
44
  function onClick(): void {
28
45
  if (!props.disabled) {
29
46
  emit('click');
@@ -0,0 +1,99 @@
1
+ import { computed, getCurrentInstance, nextTick, ref } from 'vue';
2
+ import { onClickOutside } from '@vueuse/core';
3
+ import { useI18n } from 'vue-i18n';
4
+
5
+ export interface OctopusDropdownProps<T> {
6
+ options: T[];
7
+ optionLabel: keyof T & string;
8
+ optionKey?: keyof T;
9
+ isDisabled?: boolean;
10
+ placeholder?: string;
11
+ }
12
+
13
+ export function useOctopusDropdown<T>(
14
+ props: OctopusDropdownProps<T>,
15
+ emitSearch: (query: string) => void,
16
+ idPrefix: string = 'octopus-dropdown'
17
+ ) {
18
+ const { t } = useI18n();
19
+ const instance = getCurrentInstance();
20
+
21
+ const searchQuery = ref('');
22
+ const isOpen = ref(false);
23
+ const isHovered = ref(false);
24
+ const containerRef = ref<HTMLElement | null>(null);
25
+ const inputRef = ref<HTMLInputElement | null>(null);
26
+
27
+ const computedId = computed(() => `${idPrefix}-${instance?.uid}`);
28
+
29
+ // vnode.props contains the raw parent-provided props before defineEmits strips them
30
+ // from attrs, so onSearch is present whenever the parent writes @search="...".
31
+ const hasSearchListener = computed(() => !!instance?.vnode.props?.onSearch);
32
+
33
+ const displayedOptions = computed((): T[] => {
34
+ if (hasSearchListener.value) {
35
+ return props.options;
36
+ }
37
+ const query = searchQuery.value.toLowerCase();
38
+ if (!query) {
39
+ return props.options;
40
+ }
41
+ return props.options.filter((option) =>
42
+ getLabel(option).toLowerCase().includes(query)
43
+ );
44
+ });
45
+
46
+ const inputPlaceholder = computed(() => props.placeholder ?? t('Search'));
47
+
48
+ function getLabel(option: T): string {
49
+ return option[props.optionLabel] as string;
50
+ }
51
+
52
+ function openDropdown(): void {
53
+ if (props.isDisabled) {
54
+ return;
55
+ }
56
+ isOpen.value = true;
57
+ nextTick(() => {
58
+ inputRef.value?.focus();
59
+ });
60
+ }
61
+
62
+ function closeDropdown(): void {
63
+ isOpen.value = false;
64
+ searchQuery.value = '';
65
+ }
66
+
67
+ function toggleDropdown(): void {
68
+ if (isOpen.value) {
69
+ closeDropdown();
70
+ } else {
71
+ openDropdown();
72
+ }
73
+ }
74
+
75
+ function handleInput(): void {
76
+ if (hasSearchListener.value) {
77
+ emitSearch(searchQuery.value);
78
+ }
79
+ }
80
+
81
+ onClickOutside(containerRef, closeDropdown);
82
+
83
+ return {
84
+ searchQuery,
85
+ isOpen,
86
+ isHovered,
87
+ containerRef,
88
+ inputRef,
89
+ computedId,
90
+ hasSearchListener,
91
+ displayedOptions,
92
+ inputPlaceholder,
93
+ getLabel,
94
+ openDropdown,
95
+ closeDropdown,
96
+ toggleDropdown,
97
+ handleInput,
98
+ };
99
+ }
@@ -0,0 +1,45 @@
1
+ import { ref, toValue, onMounted, onBeforeUnmount, type Ref, type MaybeRef } from 'vue';
2
+
3
+ /**
4
+ * Detects whether a sticky element is currently stuck (fixed at its `top` offset)
5
+ * or in its natural DOM position.
6
+ *
7
+ * A 1px invisible sentinel element is inserted immediately before the sticky element.
8
+ * When that sentinel scrolls out of the IntersectionObserver's root viewport (adjusted
9
+ * by `topOffset`), the element is considered stuck.
10
+ *
11
+ * `topOffset` must match the element's CSS `top` value in pixels. It is read once at
12
+ * mount time. Pass a `Ref<number>` if the value is computed asynchronously (e.g. from
13
+ * a CSS variable set by JavaScript after first render).
14
+ */
15
+ export function useSticky(elementRef: Ref<HTMLElement | null>, topOffset: MaybeRef<number> = 0) {
16
+ const isStuck = ref(false);
17
+ let sentinel: HTMLElement | null = null;
18
+ let observer: IntersectionObserver | null = null;
19
+
20
+ onMounted(() => {
21
+ const el = elementRef.value;
22
+ if (!el) {
23
+ return;
24
+ }
25
+
26
+ const offset = toValue(topOffset);
27
+
28
+ sentinel = document.createElement('div');
29
+ sentinel.style.cssText = 'height:1px;pointer-events:none;';
30
+ el.parentNode!.insertBefore(sentinel, el);
31
+
32
+ observer = new IntersectionObserver(
33
+ ([entry]) => { isStuck.value = !entry.isIntersecting; },
34
+ { rootMargin: `-${offset}px 0px 0px 0px` }
35
+ );
36
+ observer.observe(sentinel);
37
+ });
38
+
39
+ onBeforeUnmount(() => {
40
+ observer?.disconnect();
41
+ sentinel?.remove();
42
+ });
43
+
44
+ return { isStuck };
45
+ }
@@ -1,23 +1,19 @@
1
1
  <template>
2
- <ClassicMultiselect
3
- id="group-chooser"
4
- ref="selectGroup"
2
+ <OctopusMultiselect
3
+ :placeholder="placeholder ?? $t('Search - Emission groups placeholder')"
4
+ :selected="groups"
5
+ :options="allGroups"
6
+ :no-border="noBorder"
5
7
  option-label="name"
6
- :placeholder="$t('Search - Emission groups placeholder')"
7
- :max-element="maxElement"
8
- width="400px"
9
- in-modal
10
- :option-chosen="groups"
11
- multiple
12
- @on-search="onSearch"
13
- @selected="emitSelected"
8
+ option-key="groupId"
9
+ @update:selected="emitSelected"
14
10
  />
15
11
  </template>
16
12
 
17
13
  <script setup lang="ts">
18
- import { useTemplateRef } from "vue";
14
+ import { onMounted, ref } from "vue";
19
15
 
20
- import ClassicMultiselect from "../../form/ClassicMultiselect.vue";
16
+ import OctopusMultiselect from "../../form/OctopusMultiselect.vue";
21
17
  import { groupsApi, EmissionGroup } from "../../../api/groupsApi";
22
18
 
23
19
  //Props
@@ -26,29 +22,31 @@ const props = defineProps<{
26
22
  organisationId?: string|Array<string>;
27
23
  /** Currently selected groups */
28
24
  groups: Array<EmissionGroup>;
25
+ /** Change default placeholder */
26
+ placeholder?: string;
27
+ /** Disable borders */
28
+ noBorder?: boolean;
29
29
  }>();
30
30
 
31
31
  //Emits
32
- const emit = defineEmits(["update:groups"]);
32
+ const emit = defineEmits<{
33
+ (e: "update:groups", groups: Array<EmissionGroup>): void;
34
+ }>();
33
35
 
34
36
  //Data
35
- const maxElement = 50;
36
- const selectGroupRef = useTemplateRef('selectGroup');
37
+ const maxElement = 200;
38
+ const allGroups = ref<Array<EmissionGroup>>([]);
37
39
 
38
- //Methods
39
- async function onSearch(query?: string): Promise<void> {
40
+ onMounted(async() => {
40
41
  const response = await groupsApi.search({
41
42
  first: 0,
42
43
  size: maxElement,
43
- search: query,
44
44
  organisationIds: [props.organisationId].flat(),
45
45
  });
46
46
 
47
- selectGroupRef.value!.afterSearch(
48
- response.result.filter(g => g.emissionIds?.length ?? 0 > 0),
49
- response.count
50
- );
51
- }
47
+ // Only groups with emissions are available
48
+ allGroups.value = response.result.filter(g => g.emissionIds?.length ?? 0 > 0);
49
+ });
52
50
 
53
51
  function emitSelected(option: Array<EmissionGroup>) {
54
52
  emit("update:groups", option);
@@ -5,7 +5,7 @@
5
5
  :key="option.value"
6
6
  type="button"
7
7
  class="btn"
8
- :class="{ active: modelValue.includes(option.value) }"
8
+ :class="{ active: value.includes(option.value) }"
9
9
  @click="toggle(option.value)"
10
10
  >
11
11
  {{ option.label }}
@@ -13,28 +13,30 @@
13
13
  </div>
14
14
  </template>
15
15
 
16
- <script setup lang="ts">
17
- export interface ButtonGroupOption {
16
+ <script setup lang="ts" generic="T">
17
+ export interface ButtonGroupOption<T> {
18
18
  label: string;
19
- value: string;
19
+ value: T;
20
20
  }
21
21
 
22
22
  const props = defineProps<{
23
- options: ButtonGroupOption[];
24
- modelValue: string[];
23
+ /** Currently selected values */
24
+ value: T[];
25
+ /** Possible values */
26
+ options: ButtonGroupOption<T>[];
25
27
  }>();
26
28
 
27
29
  const emit = defineEmits<{
28
- 'update:modelValue': [values: string[]];
30
+ (e: 'update:value', values: T[]): void;
29
31
  }>();
30
32
 
31
- function toggle(value: string): void {
32
- const isSelected = props.modelValue.includes(value);
33
- if (isSelected && props.modelValue.length <= 1) { return; }
33
+ function toggle(value: T): void {
34
+ const isSelected = props.value.includes(value);
35
+ if (isSelected && props.value.length <= 1) { return; }
34
36
  const newValues = isSelected
35
- ? props.modelValue.filter(v => v !== value)
36
- : [...props.modelValue, value];
37
- emit('update:modelValue', newValues);
37
+ ? props.value.filter(v => v !== value)
38
+ : [...props.value, value];
39
+ emit('update:value', newValues);
38
40
  }
39
41
  </script>
40
42
 
@@ -17,7 +17,14 @@
17
17
  :required="displayRequired"
18
18
  @change="onChange($event.target.value)"
19
19
  >
20
- <option v-if="placeholder" value="" disabled selected>{{ placeholder }}</option>
20
+ <option
21
+ v-if="placeholder"
22
+ value=""
23
+ disabled
24
+ selected
25
+ >
26
+ {{ placeholder }}
27
+ </option>
21
28
  <option
22
29
  v-for="option in optionsOrder"
23
30
  :key="option.title"
@@ -25,7 +32,9 @@
25
32
  :data-selenium="'select-option-' + option.value"
26
33
  :style="option.fontFamily ? 'font-family:' + option.fontFamily : ''"
27
34
  >
28
- {{ option.title }}
35
+ <slot name="option" :option="option">
36
+ {{ option.title }}
37
+ </slot>
29
38
  </option>
30
39
  </select>
31
40
  </div>