@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
@@ -2,50 +2,71 @@
2
2
  Component to display an element with its description
3
3
  -->
4
4
  <template>
5
- <article
6
- class="classic-element-container item-presentation-container mt-3"
7
- :class="vertical ? 'vertical-item' : ''"
8
- >
9
- <router-link
10
- :to="route"
11
- class="d-flex-column flex-grow-1 text-dark"
12
- :class="vertical ? 'flex-column' : ''"
5
+ <article
6
+ class="classic-element-container item-presentation-container mt-3"
7
+ :class="vertical ? 'vertical-item' : ''"
13
8
  >
14
- <div
15
- class="img-box"
16
- :class="vertical ? 'img-box-bigger' : ''"
17
- >
18
- <img
19
- v-lazy="useProxyImageUrl(imageUrl, tailleImage)"
20
- :width="tailleImage"
21
- :height="tailleImage"
22
- aria-hidden="true"
23
- :alt="name"
24
- :title="name"
9
+ <router-link
10
+ :to="route"
11
+ class="d-flex-column flex-grow-1 text-dark"
12
+ :class="vertical ? 'flex-column' : ''"
25
13
  >
14
+ <div
15
+ class="img-box"
16
+ :class="vertical ? 'img-box-bigger' : ''"
17
+ >
18
+ <img
19
+ v-lazy="useProxyImageUrl(imageUrl, tailleImage)"
20
+ :width="tailleImage"
21
+ :height="tailleImage"
22
+ aria-hidden="true"
23
+ :alt="name"
24
+ :title="name"
25
+ >
26
26
 
27
- <slot name="after-image" />
28
- </div>
27
+ <div class="tags">
28
+ <span
29
+ v-for="tag in tags"
30
+ :key="tag"
31
+ >
32
+ {{ tag }}
33
+ </span>
34
+ </div>
35
+
36
+ <slot name="after-image" />
37
+ </div>
29
38
 
30
- <div class="classic-element-text">
31
- <div class="element-name mb-2 basic-line-clamp">
32
- {{ name }}
33
- </div>
34
- <div
35
- v-if="!isPhone && description"
36
- ref="descriptionItemContainer"
37
- class="element-description htms-wysiwyg-content"
38
- >
39
- <!-- eslint-disable vue/no-v-html -->
40
- <div
41
- ref="descriptionItem"
42
- v-html="urlify(description || '')"
43
- />
44
- <!-- eslint-enable -->
45
- </div>
46
- </div>
47
- </router-link>
48
- </article>
39
+ <div
40
+ class="classic-element-text pb-0"
41
+ :class="{ 'pt-1': vertical }"
42
+ >
43
+ <div class="element-name mb-2 basic-line-clamp">
44
+ {{ name }}
45
+ </div>
46
+ <div
47
+ v-if="!isPhone && description"
48
+ ref="descriptionItemContainer"
49
+ class="element-description htms-wysiwyg-content mt-0"
50
+ >
51
+ <div v-if="additionalInfo" class="mb-2">
52
+ <div
53
+ v-for="info, index in additionalInfo"
54
+ :key="index"
55
+ class="text-secondary"
56
+ >
57
+ {{ info }}
58
+ </div>
59
+ </div>
60
+ <!-- eslint-disable vue/no-v-html -->
61
+ <div
62
+ ref="descriptionItem"
63
+ v-html="urlify(description || '')"
64
+ />
65
+ <!-- eslint-enable -->
66
+ </div>
67
+ </div>
68
+ </router-link>
69
+ </article>
49
70
  </template>
50
71
 
51
72
  <script setup lang="ts">
@@ -57,16 +78,20 @@ import { RouteLocationRaw } from "vue-router";
57
78
 
58
79
  //Props
59
80
  const props = defineProps<{
60
- /** The route to which to redirect when clicking on element */
61
- route: RouteLocationRaw|string;
62
- /** The URL to the image */
63
- imageUrl: string;
64
- /** The name of the element */
65
- name: string;
66
- /** The description of the element */
67
- description?: string;
68
- /** When true display the card vertically */
69
- vertical?: boolean;
81
+ /** The route to which to redirect when clicking on element */
82
+ route: RouteLocationRaw|string;
83
+ /** The URL to the image */
84
+ imageUrl: string;
85
+ /** The name of the element */
86
+ name: string;
87
+ /** Tags associated with the element (rubriques, themes, etc) */
88
+ tags?: Array<string>;
89
+ /** Additional info displayed below name */
90
+ additionalInfo?: Array<string>;
91
+ /** The description of the element */
92
+ description?: string;
93
+ /** When true display the card vertically */
94
+ vertical?: boolean;
70
95
  }>();
71
96
 
72
97
  //Data
@@ -81,78 +106,98 @@ const { useProxyImageUrl } = useImageProxy();
81
106
  // Computed
82
107
  // Calcul de la taille de l'image
83
108
  const tailleImage = computed(() => {
84
- // L'élément fait 400 de large à la verticale, mais on prend en compte les bordures
85
- if (props.vertical) {
86
- return '396';
87
- } else if (isPhone.value) {
88
- return '246';
89
- } else {
90
- return '250';
91
- }
109
+ // L'élément fait 400 de large à la verticale, mais on prend en compte les bordures
110
+ if (props.vertical) {
111
+ return '396';
112
+ } else if (isPhone.value) {
113
+ return '246';
114
+ } else {
115
+ return '250';
116
+ }
92
117
  });
93
118
 
94
119
  //Watch
95
120
  watch(isPhone, async () => {
96
- nextTick(() => {
97
- if (!props.description || isPhone.value) {
98
- return;
99
- }
100
- const itemDesc = descriptionItemRef?.value as HTMLElement;
101
- const itemDescContainer = descriptionItemContainerRef?.value as HTMLElement;
102
- if (
103
- itemDesc &&
104
- itemDescContainer &&
105
- itemDesc.clientHeight > itemDescContainer.clientHeight
106
- ) {
107
- itemDescContainer.classList.add("after-element-description");
108
- }
109
- });
121
+ nextTick(() => {
122
+ if (!props.description || isPhone.value) {
123
+ return;
124
+ }
125
+ const itemDesc = descriptionItemRef?.value as HTMLElement;
126
+ const itemDescContainer = descriptionItemContainerRef?.value as HTMLElement;
127
+ if (
128
+ itemDesc &&
129
+ itemDescContainer &&
130
+ itemDesc.clientHeight > itemDescContainer.clientHeight
131
+ ) {
132
+ itemDescContainer.classList.add("after-element-description");
133
+ }
134
+ });
110
135
  }, { immediate: true });
136
+
111
137
  function urlify(text:string|undefined){
112
- return displayHelper.urlify(text);
138
+ return displayHelper.urlify(text);
113
139
  }
114
140
  </script>
115
141
 
116
142
  <style scoped lang="scss">
117
143
  .octopus-app {
118
- .item-presentation-container {
119
- @media (width <= 960px) {
120
- width: 250px !important;
121
- margin-right: 0.5rem;
122
- }
144
+ .item-presentation-container {
145
+ @media (width <= 960px) {
146
+ width: 250px !important;
147
+ margin-right: 0.5rem;
148
+ }
123
149
 
124
- .element-description {
125
- height: 0;
126
- flex-grow: 1;
127
- max-height: unset;
150
+ .element-description {
151
+ height: 0;
152
+ flex-grow: 1;
153
+ max-height: unset;
128
154
 
129
- p:first-child {
130
- // Prevent unecessary space before first paragraph
131
- margin-top: 0 !important;
132
- }
155
+ p:first-child {
156
+ // Prevent unecessary space before first paragraph
157
+ margin-top: 0 !important;
158
+ }
159
+ }
133
160
  }
134
- }
135
161
 
136
- .classic-element-container.vertical-item {
137
- flex-grow: 0;
138
- width: 400px;
139
- flex-shrink: 0;
140
- }
162
+ .classic-element-container.vertical-item {
163
+ flex-grow: 0;
164
+ width: 400px;
165
+ flex-shrink: 0;
166
+ }
141
167
 
142
- .img-box {
143
- // Make it relative so that absolute elements associated with the image
144
- // are positioned within the image box
145
- position: relative;
168
+ .img-box {
169
+ // Make it relative so that absolute elements associated with the image
170
+ // are positioned within the image box
171
+ position: relative;
146
172
 
147
- @media (width <= 960px) {
148
- height: calc(var(--octopus-image-height) - 4);
173
+ @media (width <= 960px) {
174
+ height: calc(var(--octopus-image-size) - 4);
175
+ }
149
176
  }
150
- }
151
177
 
152
- .img-box-bigger {
153
- // L'élément fait 400 de large à la verticale, mais on prend en compte les bordures
154
- width: 396px;
155
- height: 396px;
156
- }
178
+ .img-box-bigger {
179
+ // L'élément fait 400 de large à la verticale, mais on prend en compte les bordures
180
+ width: 396px;
181
+ height: 396px;
182
+ }
183
+
184
+ .tags {
185
+ position: absolute;
186
+ top: 10px;
187
+ left: 10px;
188
+
189
+ display: flex;
190
+ gap: 10px;
191
+ flex-wrap: wrap;
192
+
193
+ > span {
194
+ background-color: white;
195
+ padding: .25em .75em;
196
+ border-radius: var(--octopus-border-radius);
197
+ font-size: 14px;
198
+ font-weight: bold;
199
+ color: var(--octopus-primary);
200
+ }
201
+ }
157
202
  }
158
203
  </style>
@@ -31,6 +31,7 @@
31
31
  "Emission image": "Bild zur Reihe",
32
32
  "Emission name": "Titel der Reihe",
33
33
  "No elements found. Consider changing the search query.": "Keine Ergebnisse. Bitte ändern Sie die Suchanfrage.",
34
+ "Press Enter to add this value": "Drücken Sie Enter, um diesen Wert hinzuzufügen",
34
35
  "Podcast is not visible for listeners": "Podcast ist für Hörer verborgen",
35
36
  "Validate": "Bestätigen",
36
37
  "Invalidate": "Invalidate",
@@ -31,6 +31,7 @@
31
31
  "Emission image": "Series image",
32
32
  "Emission name": "Series title",
33
33
  "No elements found. Consider changing the search query.": "No elements found. Consider changing the search query.",
34
+ "Press Enter to add this value": "Press Enter to add this value",
34
35
  "Podcast is not visible for listeners": "Podcast is not visible for listeners",
35
36
  "Validate": "Validate",
36
37
  "Invalidate": "Invalidate",
@@ -31,6 +31,7 @@
31
31
  "Emission image": "Imagen del programa",
32
32
  "Emission name": "Título del programa",
33
33
  "No elements found. Consider changing the search query.": "No hay resultados. Prueba con otros criterios de búsqueda.",
34
+ "Press Enter to add this value": "Pulsa Intro para añadir este valor",
34
35
  "Podcast is not visible for listeners": "El público no puede visualizar este pódcast",
35
36
  "Validate": "Validar",
36
37
  "Invalidate": "Invalidar",
@@ -37,6 +37,7 @@
37
37
  "Emission subtitle": "Sous-titre de l'émission",
38
38
  "No elements found. Consider changing the search query.":
39
39
  "Aucun élement ne correspond à votre recherche",
40
+ "Press Enter to add this value": "Appuyez sur Entrée pour ajouter cette valeur",
40
41
  "Podcast is not visible for listeners":
41
42
  "L'épisode n'est pas disponible pour les auditeurs",
42
43
  "Validate": "Valider",
@@ -30,6 +30,7 @@
30
30
  "Emission image": "Immagine della serie",
31
31
  "Emission name": "Titolo della serie",
32
32
  "No elements found. Consider changing the search query.": "Non è stato trovato alcun elemento. Prova a cambiare i parametri di ricerca",
33
+ "Press Enter to add this value": "Premi Invio per aggiungere questo valore",
33
34
  "Podcast is not visible for listeners": "Il podcast non è visibile agli ascoltatori",
34
35
  "Validate": "Convalidare",
35
36
  "Invalidate": "Invalidate",
@@ -31,6 +31,7 @@
31
31
  "Emission image": "Slika oddaje",
32
32
  "Emission name": "Naslov oddaje",
33
33
  "No elements found. Consider changing the search query.": "Ni zadetkov. Spremenite poizvedbo",
34
+ "Press Enter to add this value": "Pritisnite Enter, da dodate to vrednost",
34
35
  "Podcast is not visible for listeners": "Za poslušalce skrit podkast",
35
36
  "Validate": "Potrdi",
36
37
  "Invalidate": "Invalidate",
@@ -1,14 +1,13 @@
1
- import { useDayjs } from "@/components/composable/useDayjs";
1
+ import { useDayjs } from "../components/composable/useDayjs";
2
2
  import { Dayjs } from "dayjs";
3
3
  import { defineStore } from "pinia";
4
- import { reactive } from "vue";
5
4
 
6
5
  /**
7
6
  * Type for cache storage
8
7
  */
9
8
  interface CachedData<T> {
10
- /** The stored data */
11
- data: T;
9
+ /** The in-flight or resolved data, shared by concurrent callers */
10
+ data: Promise<T>;
12
11
  /** Expiration date of the cached data */
13
12
  expiration: Dayjs;
14
13
  }
@@ -16,21 +15,27 @@ interface CachedData<T> {
16
15
  export const useCacheStore = defineStore('cache', () => {
17
16
 
18
17
  const { dayjs } = useDayjs();
19
- const cachedData = reactive({} as Record<string, CachedData<unknown>>);
18
+ const cachedData: Record<string, CachedData<unknown>> = {};
20
19
 
21
20
  async function getData<T>(key: string, callback: () => Promise<T>): Promise<T> {
22
21
  let cache = cachedData[key] as CachedData<T>|undefined;
23
22
  if (isDataEmptyOrExpired(cache)) {
24
- // Retrieve data from callback
25
- const data = await callback();
26
- const expiration = dayjs().add(5, 'minutes');
23
+ // Store the promise immediately so concurrent callers share this fetch
24
+ const data = callback();
25
+ const expiration = dayjs().add(30, 'minutes');
27
26
  cache = { data, expiration };
28
27
  cachedData[key] = cache;
28
+ // Don't keep a failed fetch cached: let the next call retry
29
+ data.catch(() => invalidate(key));
29
30
  }
30
31
 
31
32
  return cache.data;
32
33
  }
33
34
 
35
+ function invalidate(key: string): void {
36
+ delete cachedData[key];
37
+ }
38
+
34
39
  function isDataEmptyOrExpired(data: CachedData<unknown>|undefined): boolean {
35
40
  if (!data) {
36
41
  return true;
@@ -38,8 +43,9 @@ export const useCacheStore = defineStore('cache', () => {
38
43
  return dayjs().isAfter(data.expiration);
39
44
  }
40
45
  }
41
-
46
+
42
47
  return {
43
- getData
44
- }
48
+ getData,
49
+ invalidate
50
+ };
45
51
  });
@@ -32,6 +32,9 @@ const state: ParamStore = {
32
32
  },
33
33
  smartLink: {
34
34
  showOnlyFirstParagraphInDescription: false
35
+ },
36
+ presentationItems: {
37
+ tags: 'none'
35
38
  }
36
39
  };
37
40
 
@@ -102,6 +105,21 @@ export interface ParamStore {
102
105
  sortCriteria?: PodcastSort;
103
106
  };
104
107
 
108
+ /** Settings for presentation items */
109
+ presentationItems: {
110
+ /** Type of tags to display on presentation items */
111
+ tags: 'none'|'iab'|'rubrique';
112
+ /**
113
+ * For rubrique type, the ID of the rubriquage the rubriques must belong to
114
+ * to be displayed. If not set, will display all rubriques.
115
+ */
116
+ tagsRubriquageId?: number;
117
+ /** Limit number of tags */
118
+ tagsLimit?: number;
119
+ /** Additional infos displayed in presentation item */
120
+ additionalInfo?: Array<'productor'|'date'>;
121
+ };
122
+
105
123
  /** Smartlink configuration */
106
124
  smartLink: {
107
125
  /** Truncate the description to the first paragraph */
@@ -143,6 +161,10 @@ const initialize = function initialize(initObject: Partial<ParamStore>): void {
143
161
  state.smartLink,
144
162
  definedProps(initObject.smartLink)
145
163
  );
164
+ state.presentationItems = Object.assign(
165
+ state.presentationItems,
166
+ definedProps(initObject.presentationItems)
167
+ );
146
168
  };
147
169
 
148
170
  export default { initialize, state };
@@ -24,12 +24,22 @@ export type OrganisationAttributes = {
24
24
  PRIVATE?: string;
25
25
  /** Live recordings enabled */
26
26
  'live.active'?: boolean;
27
+ /** Videos enabled */
28
+ 'video.active'?: boolean;
27
29
  /**
28
30
  * List of rubriquage **names**.
29
31
  * The rubriques from these rubriquage are stored in stats and can be queried
30
32
  * upon.
31
33
  */
32
34
  rubriquage4MMIdentifier?: string;
35
+ /** Enable individualized stats */
36
+ nominal_analytic?: string;
37
+ /** Email of contact for RSS feed */
38
+ RSS_CONTACT?: string;
39
+ /** */
40
+ 'allow-participant-rss'?: string;
41
+ /** Disallow RSS sharing */
42
+ noSharing?: string;
33
43
  };
34
44
 
35
45
  export interface Organisation {
@@ -1,10 +1,32 @@
1
+ /**
2
+ * Definition for background music
3
+ * Only one of armbPlaylistName, mediaPlaylistId, and mixId can be defined.
4
+ */
5
+ export interface Ambiance {
6
+ /** Name of known playlists to play */
7
+ armbPlaylistName?: string;
8
+ /** ID of media playlist to play */
9
+ mediaPlaylistId?: number;
10
+ /** ID of mix to play */
11
+ mixId?: number;
12
+ /** Frequency of playing autopromos (in number of songs) */
13
+ autopromoRatio?: number;
14
+ /** ID of the bac/playlist media from which to take autopromos */
15
+ autopromoBacId?: number;
16
+ }
17
+
1
18
  export interface Canal {
2
- id: number;
3
- organisationId: string;
4
- name: string;
5
- defaultPlaylist: string;
6
- url: string;
7
- imageUrl: string;
8
- description: string;
9
- advertisingTag: null | string;
19
+ /** ID of the canal */
20
+ id: number;
21
+ /** ID of the organisation this canal belongs to */
22
+ organisationId: string;
23
+ name: string;
24
+ /** @deprecated, see #14467, use defaultAmbiance instead */
25
+ defaultPlaylist: string;
26
+ /** Default ambiance when nothing is playing on the canal */
27
+ defaultAmbiance: Ambiance;
28
+ url: string;
29
+ imageUrl: string;
30
+ description: string;
31
+ advertisingTag: null | string;
10
32
  }
@@ -1,3 +1,5 @@
1
+ import { Ambiance } from "./canal";
2
+
1
3
  export interface TimeValue {
2
4
  hours: number;
3
5
  minutes: number;
@@ -55,11 +57,9 @@ export interface Recurrence {
55
57
  crons: Array<Cron>;
56
58
  duration: number;
57
59
  }
58
- export interface AmbianceRecurrence extends Recurrence {
59
- armbPlaylistName?: string;
60
- mediaPlaylistId?: number;
61
- mixId?: number;
62
- }
60
+
61
+ export type AmbianceRecurrence = Recurrence & Ambiance;
62
+
63
63
  export interface PlanningRecurrence extends Recurrence {
64
64
  octopusPlaylistName: string;
65
65
  octopusPlaylistId: number;
@@ -69,6 +69,7 @@ export interface PlanningRecurrence extends Recurrence {
69
69
  advertisingTag: null | string;
70
70
  adCount: number;
71
71
  }
72
+
72
73
  export interface Exclusion {
73
74
  exclusionId: number;
74
75
  validityStart: string;
@@ -49,6 +49,8 @@
49
49
  --octopus-btn-play-bg: var(--octopus-primary-less-transparent);
50
50
  --octopus-btn-play-fg: white;
51
51
  --octopus-btn-play-radius: var(--octopus-border-radius);
52
+ --octopus-btn-social-bg: var(--octopus-secondary);
53
+ --octopus-btn-social-fg: var(--octopus-primary);
52
54
 
53
55
  // Player
54
56
  // Color for the transcript background
@@ -154,7 +154,7 @@ input:not([class^="vs__"]), button:not([class^="vs__"]), select:not([class^="vs_
154
154
  align-items: center;
155
155
  justify-content: center;
156
156
  background: var(--octopus-btn-primary-bg);
157
- border: 1px solid var(--octopus-primary);
157
+ border: 1px solid var(--octopus-btn-primary-bg);
158
158
  border-radius: var(--octopus-border-radius) !important;
159
159
  color: var(--octopus-btn-primary-fg) !important;
160
160
  font-weight: 500;
@@ -234,7 +234,8 @@ input:not([class^="vs__"]), button:not([class^="vs__"]), select:not([class^="vs_
234
234
  width: 2.5rem !important;
235
235
  height: 2.5rem !important;
236
236
  padding: 0.5rem;
237
- color : var(--octopus-primary);
237
+ color : var(--octopus-btn-social-fg);
238
+ background-color : var(--octopus-btn-social-bg);
238
239
  flex-shrink: 0;
239
240
  border-radius: 50% !important;
240
241