@saooti/octopus-sdk 41.12.0-beta2 → 41.12.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.
Files changed (38) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/CHANGELOG.md +7 -1
  3. package/index.ts +1 -1
  4. package/package.json +3 -3
  5. package/src/api/index.ts +1 -0
  6. package/src/api/radioApi.ts +18 -2
  7. package/src/api/rubriquesApi.ts +68 -0
  8. package/src/components/composable/form/useOctopusDropdown.ts +5 -2
  9. package/src/components/composable/usePresentationItem.ts +78 -0
  10. package/src/components/display/emission/EmissionInlineList.vue +1 -1
  11. package/src/components/display/emission/EmissionPresentationItem.vue +25 -16
  12. package/src/components/display/podcastmaker/PodcastmakerHeader.vue +60 -45
  13. package/src/components/display/podcasts/PodcastPresentationList.vue +103 -89
  14. package/src/components/form/ClassicButtonGroup.vue +12 -6
  15. package/src/components/form/OctopusMultiselect.vue +86 -9
  16. package/src/components/form/OctopusSelect.vue +29 -2
  17. package/src/components/layouts/PresentationItem.vue +149 -104
  18. package/src/locale/de.json +1 -0
  19. package/src/locale/en.json +1 -0
  20. package/src/locale/es.json +1 -0
  21. package/src/locale/fr.json +1 -0
  22. package/src/locale/it.json +1 -0
  23. package/src/locale/sl.json +1 -0
  24. package/src/stores/CacheStore.ts +17 -11
  25. package/src/stores/ParamSdkStore.ts +22 -0
  26. package/src/stores/class/general/organisation.ts +10 -0
  27. package/src/stores/class/radio/canal.ts +30 -8
  28. package/src/stores/class/radio/recurrence.ts +6 -5
  29. package/src/style/_variables.scss +2 -0
  30. package/src/style/bootstrap.scss +3 -2
  31. package/tests/components/composable/usePresentationItem.spec.ts +163 -0
  32. package/tests/components/display/emission/EmissionPresentationItem.spec.ts +45 -0
  33. package/tests/components/display/podcasts/PodcastPresentationList.spec.ts +107 -0
  34. package/tests/components/form/ClassicButtonGroup.spec.ts +26 -0
  35. package/tests/components/form/OctopusMultiselect.spec.ts +298 -0
  36. package/tests/components/form/OctopusSelect.spec.ts +71 -0
  37. package/tests/components/layouts/PresentationItem.spec.ts +36 -0
  38. package/tests/stores/CacheStore.spec.ts +109 -0
@@ -0,0 +1 @@
1
+ {"sessionId":"92d5c13f-8d38-4dc6-a66d-c7256640dc6f","pid":10553,"procStart":"32347","acquiredAt":1783406560303}
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CHANGELOG
2
2
 
3
- ## 41.12.0 (En cours)
3
+ ## 41.12.0 (13/07/2026)
4
4
 
5
5
  **Features**
6
6
 
@@ -15,15 +15,21 @@
15
15
  - Mise en commun de code entre `OctopusSelect` & `OctopusMultiselect`
16
16
  - Ajustement de `OctopusMultiselect` pour fonctionner correctement avec des
17
17
  objets identiques avec références différentes
18
+ - Ajout de nouvelles options à `OctopusMultiselect` pour permettre la saisie
19
+ de données et avoir une meilleure UX en cas de longue liste
18
20
  - Ajout de nouvelles classes utilitaires dans le css
19
21
  - Ajout de nouvelles options d'apparence pour `ClassicButton`
20
22
  - Changement propriété principale de `ClassicButtonGroup` pour plus de cohérence
21
23
  - Ajout de variables CSS pour configurer l'aspect de divers boutons
24
+ - Ajout d'une propriété SDK permettant d'afficher des tags sur les éléments de
25
+ type `PresentationItem`
26
+ - Ajout de l'api `rubriqueApi`
22
27
 
23
28
  **Fixes**
24
29
 
25
30
  - Correction d'une exception dans `deepEquals` si la comparaison est faite entre
26
31
  une variable primitive et un objet
32
+ - Correction du placement du header `PodcastmakerHeader`
27
33
 
28
34
  **Misc**
29
35
 
package/index.ts CHANGED
@@ -194,7 +194,7 @@ export {
194
194
  type ProviderTts,
195
195
  type Voice
196
196
  } from "./src/stores/class/transcript/transcriptParams.ts";
197
- export { type Canal } from "./src/stores/class/radio/canal.ts";
197
+ export * from "./src/stores/class/radio/canal";
198
198
  export { type Cartouchier } from "./src/stores/class/cartouchier/cartouchier.ts";
199
199
  export { type Cartouche, emptyCartouche } from "./src/stores/class/cartouchier/cartouche.ts";
200
200
  export { type PlaylistMedia } from "./src/stores/class/radio/playlistMedia";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saooti/octopus-sdk",
3
- "version": "41.12.0-beta2",
3
+ "version": "41.12.0",
4
4
  "private": false,
5
5
  "description": "Javascript SDK for using octopus",
6
6
  "author": "Saooti",
@@ -51,7 +51,7 @@
51
51
  "typescript-eslint": "^8.47.0",
52
52
  "video.js": "^8.23.6",
53
53
  "videojs-quality-selector-hls": "^1.1.1",
54
- "vite": "^6.4.1",
54
+ "vite": "^8.0.16",
55
55
  "vite-bundle-visualizer": "^1.2.1",
56
56
  "vue-material-design-icons": "^5.3.1",
57
57
  "vue-recaptcha": "^2.0.3",
@@ -64,7 +64,7 @@
64
64
  "@stylistic/eslint-plugin": "*",
65
65
  "@types/sockjs-client": "^1.5.4",
66
66
  "@types/webpack-env": "^1.18.8",
67
- "@vitejs/plugin-vue": "^5.2.4",
67
+ "@vitejs/plugin-vue": "^6.0.7",
68
68
  "@vue/test-utils": "^2.4.6",
69
69
  "eslint": "^9.39.1",
70
70
  "eslint-plugin-vue": "10.9.1",
package/src/api/index.ts CHANGED
@@ -7,3 +7,4 @@ export { podcastApi, PodcastSort, type PodcastSearchOptions } from "./podcastApi
7
7
  export { radioApi } from "./radioApi";
8
8
  export * from "./aggregatorsApi";
9
9
  export * from "./mediathequeApi";
10
+ export * from "./rubriquesApi";
@@ -1,4 +1,4 @@
1
- import { Canal } from "../stores/class/radio/canal";
1
+ import { Ambiance, Canal } from "../stores/class/radio/canal";
2
2
  import classicApi from "./classicApi";
3
3
  import { ModuleApi } from "./apiConnection";
4
4
 
@@ -26,7 +26,23 @@ async function getAll(organisationId: string): Promise<Array<Canal>> {
26
26
  });
27
27
  }
28
28
 
29
+ /**
30
+ * Set the default ambiance for the given canal
31
+ * @param canalId ID of the canal to change
32
+ * @param ambiance New ambiance settings
33
+ * @returns The modified ambiance
34
+ */
35
+ async function setDefaultAmbiance(canalId: number, ambiance: Ambiance): Promise<Ambiance> {
36
+ return await classicApi.putData<Ambiance>({
37
+ api: ModuleApi.RADIO,
38
+ path: `ambiance/canal/${canalId}/default`,
39
+ dataToSend: ambiance
40
+ });
41
+
42
+ }
43
+
29
44
  export const radioApi = {
30
45
  get,
31
- getAll
46
+ getAll,
47
+ setDefaultAmbiance
32
48
  };
@@ -0,0 +1,68 @@
1
+ import { classicApi, ModuleApi } from "@saooti/octopus-sdk";
2
+ import { Rubrique } from "@/stores/class/rubrique/rubrique";
3
+ import { Rubriquage } from "@/stores/class/rubrique/rubriquage";
4
+ import { useCacheStore } from "../stores/CacheStore";
5
+
6
+ /**
7
+ * Find rubriquages according to criterias
8
+ * @param organisationIds List of organisation IDs for which to retrieve the
9
+ * rubriquage
10
+ * @param searchOptions Additional search options
11
+ */
12
+ async function searchRubriquages(organisationIds: Array<string>, searchOptions?: {
13
+ rubriquageId?: number;
14
+ organisationId?: string|Array<string>;
15
+ query?: string;
16
+ }): Promise<Array<Rubriquage>> {
17
+ return classicApi.fetchData<Array<Rubriquage>>({
18
+ api: ModuleApi.DEFAULT,
19
+ path: 'rubriquage/find',
20
+ parameters: {
21
+ organisationId: organisationIds,
22
+ ...searchOptions
23
+ }
24
+ });
25
+ }
26
+
27
+ async function searchRubriques(searchOptions?: {
28
+ rubriquageId?: number;
29
+ organisationId?: string|Array<string>;
30
+ query?: string;
31
+ }): Promise<Array<Rubrique>> {
32
+ return classicApi.fetchData<Array<Rubrique>>({
33
+ api: ModuleApi.DEFAULT,
34
+ path: 'rubrique/search',
35
+ parameters: searchOptions
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Fetch rubrique data by ID
41
+ * @see getCachedRubrique
42
+ * @param rubriqueId ID of the rubrique to fetch
43
+ * @returns The rubrique
44
+ */
45
+ async function getRubrique(rubriqueId: number): Promise<Rubrique> {
46
+ return classicApi.fetchData<Rubrique>({
47
+ api: ModuleApi.DEFAULT,
48
+ path: `rubrique/${rubriqueId}`
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Fetch rubrique data by ID
54
+ * If the rubrique is available in the cache, return it from the cache
55
+ * @param rubriqueId ID of the rubrique to fetch
56
+ * @returns The rubrique
57
+ */
58
+ async function getCachedRubrique(rubriqueId: number): Promise<Rubrique> {
59
+ const cacheStore = useCacheStore();
60
+ return cacheStore.getData(`rubrique-${rubriqueId}`, () => getRubrique(rubriqueId));
61
+ }
62
+
63
+ export const rubriquesApi = {
64
+ getRubrique,
65
+ getCachedRubrique,
66
+ searchRubriquages,
67
+ searchRubriques
68
+ };
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
4
4
 
5
5
  export interface OctopusDropdownProps<T> {
6
6
  options: T[];
7
- optionLabel: keyof T & string;
7
+ optionLabel?: keyof T & string;
8
8
  optionKey?: keyof T;
9
9
  isDisabled?: boolean;
10
10
  placeholder?: string;
@@ -51,7 +51,10 @@ export function useOctopusDropdown<T>(
51
51
  const inputPlaceholder = computed(() => props.placeholder ?? t('Search'));
52
52
 
53
53
  function getLabel(option: T): string {
54
- return option[props.optionLabel] as string;
54
+ if (props.optionLabel) {
55
+ return option[props.optionLabel] as string;
56
+ }
57
+ return option as unknown as string;
55
58
  }
56
59
 
57
60
  function openDropdown(): void {
@@ -0,0 +1,78 @@
1
+ import { Emission } from "@/stores/class/general/emission";
2
+ import { state } from "../../stores/ParamSdkStore";
3
+ import { useGeneralStore } from "../../stores/GeneralStore";
4
+ import { Podcast } from "../../stores/class/general/podcast";
5
+ import { rubriquesApi } from "../../api";
6
+ import { useDayjs } from "./useDayjs";
7
+
8
+ export const usePresentationItem = () => {
9
+ const generalStore = useGeneralStore();
10
+ const { formatDate } = useDayjs();
11
+
12
+ function iabTags(emission: Emission): Array<string> {
13
+ return generalStore.storedCategories
14
+ .filter(cat => emission.iabIds.includes(cat.id))
15
+ .map(cat => cat.name);
16
+ }
17
+
18
+ async function rubriqueTags(element: Podcast|Emission): Promise<Array<string>> {
19
+ const rubriqueIds: Array<number> = [];
20
+ if (element.rubriqueIds) {
21
+ rubriqueIds.push(...element.rubriqueIds);
22
+ }
23
+ if ('emission' in element && element.emission.rubriqueIds) {
24
+ rubriqueIds.push(...element.emission.rubriqueIds);
25
+ }
26
+ const promises = rubriqueIds.map(rubriquesApi.getCachedRubrique);
27
+ const rubriques = await Promise.all(promises);
28
+
29
+ if (state.presentationItems.tagsRubriquageId) {
30
+ const rubriquageId = state.presentationItems.tagsRubriquageId;
31
+ return rubriques.filter(r => r.rubriquageId === rubriquageId).map(r => r.name);
32
+ } else {
33
+ return rubriques.map(r => r.name);
34
+ }
35
+ }
36
+
37
+ async function tagsFor(element: Podcast|Emission): Promise<Array<string>|undefined> {
38
+ const type = state.presentationItems.tags;
39
+ let tags: Array<string>|undefined = undefined;
40
+ if (type === 'iab') {
41
+ const emission = 'emission' in element ? element.emission : element;
42
+ tags = iabTags(emission);
43
+ } else if (type === 'rubrique') {
44
+ tags = await rubriqueTags(element);
45
+ }
46
+
47
+ if (tags && state.presentationItems.tagsLimit) {
48
+ return tags.slice(0, state.presentationItems.tagsLimit);
49
+ }
50
+
51
+ return tags;
52
+ }
53
+
54
+ function additionalInfoFor(element: Podcast|Emission): Array<string>|undefined {
55
+ const prop = state.presentationItems.additionalInfo;
56
+ if (!prop?.length) {
57
+ return undefined;
58
+ }
59
+
60
+ const podcast = 'emission' in element ? element : null;
61
+ const emission = podcast?.emission ?? element as Emission;
62
+
63
+ return prop.map(property => {
64
+ if (property === 'date') {
65
+ if (podcast?.pubDate) {
66
+ return formatDate(podcast.pubDate);
67
+ } else {
68
+ return null;
69
+ }
70
+ } else if (property === 'productor') {
71
+ return emission.orga.name;
72
+ }
73
+ return null;
74
+ }).filter(p => p !== null);
75
+ }
76
+
77
+ return { tagsFor, additionalInfoFor };
78
+ };
@@ -30,7 +30,7 @@
30
30
  <router-link
31
31
  v-if="href"
32
32
  :to="href"
33
- class="btn btn-primary align-self-center w-fit-content m-4"
33
+ class="btn btn-primary align-self-center w-fit-content m-4 icon-available"
34
34
  >
35
35
  {{ buttonText }}
36
36
  </router-link>
@@ -2,33 +2,42 @@
2
2
  Component to display an emission with its description
3
3
  -->
4
4
  <template>
5
- <PresentationItem
6
- :name="emission.name"
7
- :route="route"
8
- :image-url="emission.imageUrl"
9
- :description="isDescription ? emission.description : undefined"
10
- :vertical="isVertical"
11
- />
5
+ <PresentationItem
6
+ :name="emission.name"
7
+ :route="route"
8
+ :image-url="emission.imageUrl"
9
+ :description="isDescription ? emission.description : undefined"
10
+ :vertical="isVertical"
11
+ :tags="tags"
12
+ :additional-info="additionalInfoFor(emission)"
13
+ />
12
14
  </template>
13
15
 
14
16
  <script setup lang="ts">
15
17
  import { Emission } from "@/stores/class/general/emission";
16
18
  import PresentationItem from "../../layouts/PresentationItem.vue";
17
- import { computed } from "vue";
19
+ import { computed, ref, watch } from "vue";
18
20
  import { RouteLocationRaw } from "vue-router";
21
+ import { usePresentationItem } from "../../composable/usePresentationItem";
19
22
 
20
23
  //Props
21
24
  const props = defineProps<{
22
- /** The emission to display */
23
- emission: Emission;
24
- /** When true display the card vertically */
25
- isVertical?: boolean;
26
- /** When true also display the description */
27
- isDescription?: boolean;
25
+ /** The emission to display */
26
+ emission: Emission;
27
+ /** When true display the card vertically */
28
+ isVertical?: boolean;
29
+ /** When true also display the description */
30
+ isDescription?: boolean;
28
31
  }>();
29
32
 
33
+ const tags = ref([]);
34
+ const { tagsFor, additionalInfoFor } = usePresentationItem();
35
+
36
+ watch(props.emission, async () => {
37
+ tags.value = await tagsFor(props.emission);
38
+ }, { immediate: true });
39
+
30
40
  const route = computed((): RouteLocationRaw => {
31
- return { name: 'emission', params: { emissionId: props.emission.emissionId } };
41
+ return { name: 'emission', params: { emissionId: props.emission.emissionId } };
32
42
  });
33
-
34
43
  </script>
@@ -1,72 +1,87 @@
1
1
  <template>
2
- <div class="page-element-title-container">
3
- <div class="page-element-title">
4
- <h1>{{ pageTitle }}</h1>
2
+ <div class="page-element-title-container">
3
+ <div class="page-element-title">
4
+ <h1>{{ pageTitle }}</h1>
5
+ </div>
6
+ <div class="page-element-bg" :style="backgroundDisplay" />
5
7
  </div>
6
- <div class="page-element-bg" :style="backgroundDisplay" />
7
- </div>
8
8
  </template>
9
9
 
10
10
  <script setup lang="ts">
11
- import { computed } from "vue";
12
- import {useImageProxy} from "../../composable/useImageProxy";
11
+ import { computed, onMounted } from "vue";
12
+ import { useImageProxy } from "../../composable/useImageProxy";
13
13
 
14
14
  //Props
15
15
  const props = defineProps({
16
- pageTitle: { default: undefined, type: String },
17
- imgUrl: { default: undefined, type: String },
16
+ pageTitle: { default: undefined, type: String },
17
+ imgUrl: { default: undefined, type: String },
18
18
  })
19
19
 
20
+ onMounted(() => {
21
+ const elt = document.querySelector('.page-element-title-container');
22
+
23
+ // Retrieve header
24
+ const header = document.getElementsByTagName('header');
25
+ if (header.length) {
26
+ // Get header height
27
+ const height = (header[0] as HTMLElement).offsetHeight;
28
+ // Update PM header position according to header height
29
+ elt.style.setProperty('--top', height + 'px');
30
+ }
31
+ });
32
+
20
33
  //Composables
21
34
  const { useProxyImageUrl } = useImageProxy();
22
35
 
23
36
  //Computed
24
37
  const backgroundDisplay = computed(() => {
25
- if (!props.imgUrl) {
26
- return "";
27
- }
28
- return `background-image: url('${useProxyImageUrl(
29
- props.imgUrl,
30
- "250",
31
- )}');`;
38
+ if (!props.imgUrl) {
39
+ return "";
40
+ }
41
+ return `background-image: url('${useProxyImageUrl(
42
+ props.imgUrl,
43
+ "250",
44
+ )}');`;
32
45
  });
33
46
  </script>
34
47
 
35
48
  <style lang="scss">
36
49
  .octopus-app {
37
- .page-element.page-element-podcastmaker {
38
- margin-top: 11rem;
39
- }
50
+ .page-element.page-element-podcastmaker {
51
+ margin-top: 11rem;
52
+ }
40
53
 
41
- .page-element-title-container {
42
- background: black;
43
- position: absolute;
44
- right: 0;
45
- left: 0;
46
- top: 0;
54
+ .page-element-title-container {
55
+ --top: 0;
47
56
 
48
- .page-element-bg {
49
- height: 15rem;
50
- opacity: 0.5;
51
- filter: blur(8px);
52
- background-position: center;
53
- background-repeat: no-repeat;
54
- background-size: cover;
55
- }
57
+ background: black;
58
+ position: absolute;
59
+ right: 0;
60
+ left: 0;
61
+ top: var(--top);
62
+
63
+ .page-element-bg {
64
+ height: 15rem;
65
+ opacity: 0.5;
66
+ filter: blur(8px);
67
+ background-position: center;
68
+ background-repeat: no-repeat;
69
+ background-size: cover;
70
+ }
56
71
 
57
- .page-element-title {
58
- position: absolute;
59
- inset: 0;
60
- z-index: 2;
61
- display: flex;
62
- justify-content: center;
63
- align-items: center;
72
+ .page-element-title {
73
+ position: absolute;
74
+ inset: 0;
75
+ z-index: 2;
76
+ display: flex;
77
+ justify-content: center;
78
+ align-items: center;
64
79
 
65
- h1 {
66
- color: white !important;
67
- font-size: 2rem;
68
- }
80
+ h1 {
81
+ color: white !important;
82
+ font-size: 2rem;
83
+ }
84
+ }
69
85
  }
70
- }
71
86
  }
72
87
  </style>