@saooti/octopus-sdk 41.11.1 → 41.12.0-beta2

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/eslint-config.mjs +10 -2
  3. package/index.ts +5 -2
  4. package/package.json +4 -1
  5. package/src/api/radioApi.ts +14 -1
  6. package/src/components/buttons/ClassicButton.vue +18 -1
  7. package/src/components/composable/form/useOctopusDropdown.ts +104 -0
  8. package/src/components/composable/useSticky.ts +45 -0
  9. package/src/components/display/emission/EmissionGroupChooser.vue +22 -24
  10. package/src/components/display/podcasts/PodcastPlayButton.vue +3 -3
  11. package/src/components/form/ClassicButtonGroup.vue +15 -13
  12. package/src/components/form/ClassicSelect.vue +11 -2
  13. package/src/components/form/OctopusMultiselect.vue +236 -109
  14. package/src/components/form/OctopusSelect.vue +278 -0
  15. package/src/components/misc/ClassicPopover.vue +9 -2
  16. package/src/components/misc/modal/ClassicModal.vue +3 -0
  17. package/src/components/misc/player/elements/PlayerPlayButton.vue +22 -18
  18. package/src/components/pages/PlaylistsPage.vue +1 -0
  19. package/src/helper/colorFromString.ts +12 -5
  20. package/src/helper/equals.ts +21 -9
  21. package/src/stores/ParamSdkStore.ts +1 -0
  22. package/src/stores/class/general/organisation.ts +6 -0
  23. package/src/stores/class/general/podcast.ts +8 -0
  24. package/src/stores/class/rubrique/rubrique.ts +1 -1
  25. package/src/style/_utilities.scss +1 -0
  26. package/src/style/_variables.scss +11 -0
  27. package/src/style/bootstrap.scss +9 -2
  28. package/tests/components/form/ClassicButtonGroup.spec.ts +10 -10
  29. package/tests/components/form/OctopusMultiselect.spec.ts +84 -39
  30. package/tests/components/form/OctopusSelect.spec.ts +168 -0
  31. package/tsconfig.json +3 -7
  32. package/tsconfig.test.json +13 -0
  33. package/vitest.config.js +2 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
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
+ - Ajout de variables CSS pour configurer l'aspect de divers boutons
22
+
23
+ **Fixes**
24
+
25
+ - Correction d'une exception dans `deepEquals` si la comparaison est faite entre
26
+ une variable primitive et un objet
27
+
28
+ **Misc**
29
+
30
+ - Mise à jour des règles eslint
31
+
3
32
  ## 41.11.1 (17/06/2026)
4
33
 
5
34
  **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-beta2",
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,104 @@
1
+ import { computed, getCurrentInstance, nextTick, ref } from 'vue';
2
+ import { type MaybeElementRef, 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
+ // Extra elements to exclude from the click-outside check.
18
+ // Needed when the dropdown is teleported outside containerRef (e.g. to body):
19
+ // without this, clicking inside the teleported dropdown triggers closeDropdown
20
+ // because it is no longer a DOM descendant of containerRef.
21
+ ignore?: MaybeElementRef[]
22
+ ) {
23
+ const { t } = useI18n();
24
+ const instance = getCurrentInstance();
25
+
26
+ const searchQuery = ref('');
27
+ const isOpen = ref(false);
28
+ const isHovered = ref(false);
29
+ const containerRef = ref<HTMLElement | null>(null);
30
+ const inputRef = ref<HTMLInputElement | null>(null);
31
+
32
+ const computedId = computed(() => `${idPrefix}-${instance?.uid}`);
33
+
34
+ // vnode.props contains the raw parent-provided props before defineEmits strips them
35
+ // from attrs, so onSearch is present whenever the parent writes @search="...".
36
+ const hasSearchListener = computed(() => !!instance?.vnode.props?.onSearch);
37
+
38
+ const displayedOptions = computed((): T[] => {
39
+ if (hasSearchListener.value) {
40
+ return props.options;
41
+ }
42
+ const query = searchQuery.value.toLowerCase();
43
+ if (!query) {
44
+ return props.options;
45
+ }
46
+ return props.options.filter((option) =>
47
+ getLabel(option).toLowerCase().includes(query)
48
+ );
49
+ });
50
+
51
+ const inputPlaceholder = computed(() => props.placeholder ?? t('Search'));
52
+
53
+ function getLabel(option: T): string {
54
+ return option[props.optionLabel] as string;
55
+ }
56
+
57
+ function openDropdown(): void {
58
+ if (props.isDisabled) {
59
+ return;
60
+ }
61
+ isOpen.value = true;
62
+ nextTick(() => {
63
+ inputRef.value?.focus();
64
+ });
65
+ }
66
+
67
+ function closeDropdown(): void {
68
+ isOpen.value = false;
69
+ searchQuery.value = '';
70
+ }
71
+
72
+ function toggleDropdown(): void {
73
+ if (isOpen.value) {
74
+ closeDropdown();
75
+ } else {
76
+ openDropdown();
77
+ }
78
+ }
79
+
80
+ function handleInput(): void {
81
+ if (hasSearchListener.value) {
82
+ emitSearch(searchQuery.value);
83
+ }
84
+ }
85
+
86
+ onClickOutside(containerRef, closeDropdown, ignore?.length ? { ignore } : undefined);
87
+
88
+ return {
89
+ searchQuery,
90
+ isOpen,
91
+ isHovered,
92
+ containerRef,
93
+ inputRef,
94
+ computedId,
95
+ hasSearchListener,
96
+ displayedOptions,
97
+ inputPlaceholder,
98
+ getLabel,
99
+ openDropdown,
100
+ closeDropdown,
101
+ toggleDropdown,
102
+ handleInput,
103
+ };
104
+ }
@@ -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);
@@ -359,15 +359,15 @@ async function play(isVideo: boolean): Promise<void> {
359
359
  left: 0;
360
360
  font-size: 1rem;
361
361
  color: white;
362
- background-color: var(--octopus-primary-less-transparent);
363
- border-radius: var(--octopus-border-radius);
362
+ background-color: var(--octopus-btn-play-bg);
363
+ border-radius: var(--octopus-btn-play-radius);
364
364
 
365
365
  @media (width <= 960px) {
366
366
  font-size: 0.8rem;
367
367
  }
368
368
 
369
369
  button{
370
- color: white;
370
+ color: var(--octopus-btn-play-fg);
371
371
  background-color: transparent;
372
372
  border: 0;
373
373
  display: flex;
@@ -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>