@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.
- package/CHANGELOG.md +5 -1
- package/index.ts +1 -1
- package/package.json +1 -1
- package/src/api/index.ts +1 -0
- package/src/api/radioApi.ts +18 -2
- package/src/api/rubriquesApi.ts +68 -0
- package/src/components/composable/usePresentationItem.ts +78 -0
- package/src/components/display/emission/EmissionInlineList.vue +1 -1
- package/src/components/display/emission/EmissionPresentationItem.vue +25 -16
- package/src/components/display/podcastmaker/PodcastmakerHeader.vue +60 -45
- package/src/components/display/podcasts/PodcastPresentationList.vue +103 -89
- package/src/components/form/ClassicButtonGroup.vue +12 -6
- package/src/components/layouts/PresentationItem.vue +149 -104
- package/src/stores/CacheStore.ts +17 -11
- package/src/stores/ParamSdkStore.ts +22 -0
- package/src/stores/class/general/organisation.ts +7 -3
- package/src/stores/class/radio/canal.ts +30 -8
- package/src/stores/class/radio/recurrence.ts +6 -5
- package/tests/components/composable/usePresentationItem.spec.ts +163 -0
- package/tests/components/display/emission/EmissionPresentationItem.spec.ts +45 -0
- package/tests/components/display/podcasts/PodcastPresentationList.spec.ts +107 -0
- package/tests/components/form/ClassicButtonGroup.spec.ts +26 -0
- package/tests/components/layouts/PresentationItem.spec.ts +36 -0
- package/tests/stores/CacheStore.spec.ts +109 -0
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
138
|
+
return displayHelper.urlify(text);
|
|
113
139
|
}
|
|
114
140
|
</script>
|
|
115
141
|
|
|
116
142
|
<style scoped lang="scss">
|
|
117
143
|
.octopus-app {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
144
|
+
.item-presentation-container {
|
|
145
|
+
@media (width <= 960px) {
|
|
146
|
+
width: 250px !important;
|
|
147
|
+
margin-right: 0.5rem;
|
|
148
|
+
}
|
|
123
149
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
150
|
+
.element-description {
|
|
151
|
+
height: 0;
|
|
152
|
+
flex-grow: 1;
|
|
153
|
+
max-height: unset;
|
|
128
154
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
162
|
+
.classic-element-container.vertical-item {
|
|
163
|
+
flex-grow: 0;
|
|
164
|
+
width: 400px;
|
|
165
|
+
flex-shrink: 0;
|
|
166
|
+
}
|
|
141
167
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
173
|
+
@media (width <= 960px) {
|
|
174
|
+
height: calc(var(--octopus-image-size) - 4);
|
|
175
|
+
}
|
|
149
176
|
}
|
|
150
|
-
}
|
|
151
177
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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>
|
package/src/stores/CacheStore.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { useDayjs } from "
|
|
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
|
|
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
|
|
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
|
-
//
|
|
25
|
-
const data =
|
|
26
|
-
const expiration = dayjs().add(
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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;
|