@saooti/octopus-sdk 41.12.0-beta3 → 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.
@@ -24,6 +24,8 @@ const props = defineProps<{
24
24
  value: T[];
25
25
  /** Possible values */
26
26
  options: ButtonGroupOption<T>[];
27
+ /** Select only one option */
28
+ solo?: boolean;
27
29
  }>();
28
30
 
29
31
  const emit = defineEmits<{
@@ -31,12 +33,16 @@ const emit = defineEmits<{
31
33
  }>();
32
34
 
33
35
  function toggle(value: T): void {
34
- const isSelected = props.value.includes(value);
35
- if (isSelected && props.value.length <= 1) { return; }
36
- const newValues = isSelected
37
- ? props.value.filter(v => v !== value)
38
- : [...props.value, value];
39
- emit('update:value', newValues);
36
+ if (props.solo === true) {
37
+ emit('update:value', [value]);
38
+ } else {
39
+ const isSelected = props.value.includes(value);
40
+ if (isSelected && props.value.length <= 1) { return; }
41
+ const newValues = isSelected
42
+ ? props.value.filter(v => v !== value)
43
+ : [...props.value, value];
44
+ emit('update:value', newValues);
45
+ }
40
46
  }
41
47
  </script>
42
48
 
@@ -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>
@@ -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 };
@@ -32,10 +32,14 @@ export type OrganisationAttributes = {
32
32
  * upon.
33
33
  */
34
34
  rubriquage4MMIdentifier?: string;
35
- /**
36
- * Enable individualized stats
37
- */
35
+ /** Enable individualized stats */
38
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;
39
43
  };
40
44
 
41
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;