@saooti/octopus-sdk 41.7.3 → 41.8.1
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/.claude/settings.local.json +2 -1
- package/CHANGELOG.md +24 -0
- package/index.ts +3 -2
- package/package.json +1 -1
- package/src/api/emissionApi.ts +44 -1
- package/src/api/podcastApi.ts +27 -5
- package/src/components/composable/useRights.ts +1 -1
- package/src/components/composable/useSeasonsManagement.ts +43 -0
- package/src/components/display/emission/EmissionPlayerItem.vue +6 -4
- package/src/components/display/podcasts/PodcastFilterList.vue +100 -18
- package/src/components/display/podcasts/PodcastInlineListTemplate.vue +4 -1
- package/src/components/display/podcasts/PodcastList.vue +5 -1
- package/src/components/display/podcasts/PodcastModuleBox.vue +21 -4
- package/src/components/display/podcasts/PodcastSwiperList.vue +9 -3
- package/src/components/form/ClassicRadio.vue +13 -14
- package/src/components/misc/TopBar.vue +7 -1
- package/src/components/pages/EmissionPage.vue +22 -10
- package/src/locale/de.json +7 -1
- package/src/locale/en.json +7 -1
- package/src/locale/es.json +7 -1
- package/src/locale/fr.json +7 -1
- package/src/locale/it.json +7 -1
- package/src/locale/sl.json +7 -1
- package/src/stores/class/general/emission.ts +20 -0
- package/src/stores/class/general/podcast.ts +17 -0
- package/src/style/general.scss +7 -0
- package/tests/api/podcastApi.spec.ts +43 -0
- package/tests/components/composable/useSeasonsManagement.spec.ts +35 -0
- package/tests/components/display/podcasts/PodcastFilterList.spec.ts +33 -0
- package/tests/components/display/podcasts/PodcastInlineListTemplate.spec.ts +23 -0
- package/tests/components/display/podcasts/PodcastModuleBox.spec.ts +49 -22
- package/tests/components/pages/EmissionPage.spec.ts +86 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 41.8.1 (20/03/2026)
|
|
4
|
+
|
|
5
|
+
**Fix**
|
|
6
|
+
|
|
7
|
+
- Correction `EmissionPlayerItem` ne permettant pas de lire un épisode
|
|
8
|
+
|
|
9
|
+
## 41.8.0 (18/03/2026)
|
|
10
|
+
|
|
11
|
+
**Features**
|
|
12
|
+
|
|
13
|
+
- **14083** - Ajout des fonctionnalités de saisons et types d'épisodes
|
|
14
|
+
- Ajout des propriétés relatives aux saisons sur les émissions et les épisodes
|
|
15
|
+
- `PodcastFilterList` permet de regrouper les épisodes en saisons
|
|
16
|
+
- `EmissionPage` affiche ses épisodes en saisons si définies
|
|
17
|
+
- `PodcastModuleBox` affiche le numéro de saison et d'épisode du podcast si
|
|
18
|
+
définis et que le `seasonMode` permet leur affichage
|
|
19
|
+
- Ajout d'une classe `required` pour afficher une asterisque sur les champs
|
|
20
|
+
requis
|
|
21
|
+
|
|
22
|
+
**Fix**
|
|
23
|
+
|
|
24
|
+
- **14291** - Activation du bouton de génération de la transcription suite à
|
|
25
|
+
correction des droits
|
|
26
|
+
|
|
3
27
|
## 41.7.3 (11/03/2026)
|
|
4
28
|
|
|
5
29
|
**Features**
|
package/index.ts
CHANGED
|
@@ -134,6 +134,7 @@ export { useSharePlatforms, SharePlatformName, type SharePlatform } from "./src/
|
|
|
134
134
|
export { useSharePath } from "./src/components/composable/share/useSharePath.ts";
|
|
135
135
|
export { useOrgaComputed } from "./src/components/composable/useOrgaComputed.ts";
|
|
136
136
|
export { useSeoTitleUrl } from "./src/components/composable/route/useSeoTitleUrl.ts";
|
|
137
|
+
export { useSeasonsManagement } from "./src/components/composable/useSeasonsManagement.ts";
|
|
137
138
|
|
|
138
139
|
//helper
|
|
139
140
|
import domHelper from "./src/helper/domHelper.ts";
|
|
@@ -166,8 +167,8 @@ export { playlistApi } from "./src/api/playlistApi.ts";
|
|
|
166
167
|
export { podcastApi, PodcastSort, type PodcastSearchOptions } from "./src/api/podcastApi.ts";
|
|
167
168
|
|
|
168
169
|
// Types
|
|
169
|
-
export { type Emission, emptyEmissionData } from "./src/stores/class/general/emission.ts";
|
|
170
|
-
export { type Podcast, type PodcastAvailability } from "./src/stores/class/general/podcast.ts";
|
|
170
|
+
export { type Emission, SeasonMode, emptyEmissionData } from "./src/stores/class/general/emission.ts";
|
|
171
|
+
export { type Podcast, type PodcastAvailability, PodcastType } from "./src/stores/class/general/podcast.ts";
|
|
171
172
|
export { type Playlist, type PlaylistRule } from "./src/stores/class/general/playlist.ts";
|
|
172
173
|
export { type Annotations } from "./src/stores/class/general";
|
|
173
174
|
|
package/package.json
CHANGED
package/src/api/emissionApi.ts
CHANGED
|
@@ -2,6 +2,7 @@ import classicApi from "./classicApi";
|
|
|
2
2
|
import { ModuleApi } from "./apiConnection";
|
|
3
3
|
import { mapFromGetAll } from "./apiUtils";
|
|
4
4
|
import { Emission } from "@/stores/class/general/emission";
|
|
5
|
+
import { ListClassicReturn } from "@/stores/class/general/listReturn";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Retrieve an emission by ID
|
|
@@ -25,7 +26,49 @@ async function getAllById(emissionIds: Array<number>): Promise<Record<string, Em
|
|
|
25
26
|
return mapFromGetAll(emissionIds, get, 'emissionId');
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Search for emissions
|
|
32
|
+
* @param query A filter on the name of the emission
|
|
33
|
+
* @param options Optional options to filter the results
|
|
34
|
+
*/
|
|
35
|
+
async function search(query?: string, options?: {
|
|
36
|
+
/** The index of the first element to retrieve */
|
|
37
|
+
first?: number;
|
|
38
|
+
/** The number of elements to retrieve */
|
|
39
|
+
size?: number;
|
|
40
|
+
specialTreatment?: boolean;
|
|
41
|
+
distributedBy?: string;
|
|
42
|
+
/** Filter by organisation */
|
|
43
|
+
organisationId?: string|string[];
|
|
44
|
+
}): Promise<ListClassicReturn<Emission>> {
|
|
45
|
+
return classicApi.fetchData<ListClassicReturn<Emission>>({
|
|
46
|
+
api: ModuleApi.DEFAULT,
|
|
47
|
+
path: "emission/search",
|
|
48
|
+
parameters: {
|
|
49
|
+
query,
|
|
50
|
+
first: options?.first ?? 0,
|
|
51
|
+
size: options?.size ?? 10,
|
|
52
|
+
distributedBy: options?.distributedBy,
|
|
53
|
+
organisationId: options?.organisationId
|
|
54
|
+
},
|
|
55
|
+
specialTreatement: options?.specialTreatment ?? true
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Remove seasons data from emission
|
|
61
|
+
* @param emissionId The ID of the emission for which to remove the data
|
|
62
|
+
*/
|
|
63
|
+
async function resetSeasons(emissionId: number): Promise<void> {
|
|
64
|
+
return classicApi.putData({
|
|
65
|
+
api: ModuleApi.DEFAULT,
|
|
66
|
+
path: 'emission/seasons/reset/' + emissionId
|
|
67
|
+
});
|
|
68
|
+
}
|
|
28
69
|
export const emissionApi = {
|
|
29
70
|
get,
|
|
30
|
-
getAllById
|
|
71
|
+
getAllById,
|
|
72
|
+
search,
|
|
73
|
+
resetSeasons
|
|
31
74
|
};
|
package/src/api/podcastApi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Podcast, PodcastProcessingStatus, SimplifiedPodcast } from '../stores/class/general/podcast';
|
|
1
|
+
import { Podcast, PodcastProcessingStatus, PodcastType, SimplifiedPodcast } from '../stores/class/general/podcast';
|
|
2
2
|
import { ListClassicReturn } from '../stores/class/general/listReturn';
|
|
3
3
|
import { useAuthStore } from '../stores/AuthStore';
|
|
4
4
|
import classicApi from './classicApi';
|
|
@@ -17,7 +17,9 @@ export enum PodcastSort {
|
|
|
17
17
|
POPULARITY = 'POPULARITY',
|
|
18
18
|
SCORE = 'SCORE',
|
|
19
19
|
UPDATE_ASC = 'UPDATE_ASC',
|
|
20
|
-
UPDATE_DESC = 'UPDATE_DESC'
|
|
20
|
+
UPDATE_DESC = 'UPDATE_DESC',
|
|
21
|
+
/** Smart sort using seasons settings */
|
|
22
|
+
SEASONAL = 'SEASONAL'
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export enum PodcastMonetisation {
|
|
@@ -68,11 +70,15 @@ export interface PodcastSearchOptions extends Paginable<PodcastSort> {
|
|
|
68
70
|
tags?: Array<string>;
|
|
69
71
|
/** Filter by beneficiaries/rights holder reference */
|
|
70
72
|
beneficiaries?: Array<string>;
|
|
73
|
+
/** Filter by seasons */
|
|
74
|
+
season?: Array<number>;
|
|
75
|
+
/** Filter by season episode number */
|
|
76
|
+
seasonEpisode?: Array<number>;
|
|
77
|
+
/** Filter by episode type */
|
|
78
|
+
episodeType?: PodcastType;
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
async function downloadRegister(podcastId: number, parameters?: Record<string,unknown>): Promise<{ location: string; downloadId: number }> {
|
|
74
|
-
const authStore = useAuthStore();
|
|
75
|
-
|
|
76
82
|
return classicApi.fetchData<{
|
|
77
83
|
location: string;
|
|
78
84
|
downloadId: number;
|
|
@@ -137,7 +143,7 @@ function processSearchParameters(search: PodcastSearchOptions): FetchParam {
|
|
|
137
143
|
* @param adaptParameters If true, some adjustments will be made to the parameters
|
|
138
144
|
* @return A list of simplified podcasts
|
|
139
145
|
*/
|
|
140
|
-
function search(options: PodcastSearchOptions, adaptParameters?: boolean): Promise<ListClassicReturn<SimplifiedPodcast>> {
|
|
146
|
+
async function search(options: PodcastSearchOptions, adaptParameters?: boolean): Promise<ListClassicReturn<SimplifiedPodcast>> {
|
|
141
147
|
return classicApi.fetchData<ListClassicReturn<SimplifiedPodcast>>({
|
|
142
148
|
api: ModuleApi.DEFAULT,
|
|
143
149
|
path: 'v2/podcast/search',
|
|
@@ -146,6 +152,21 @@ function search(options: PodcastSearchOptions, adaptParameters?: boolean): Promi
|
|
|
146
152
|
});
|
|
147
153
|
}
|
|
148
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Count podcasts matching the filters
|
|
157
|
+
* @param options The search criterias
|
|
158
|
+
* @param adaptParameters If true, some adjustments will be made to the parameters
|
|
159
|
+
* @return A list of simplified podcasts
|
|
160
|
+
*/
|
|
161
|
+
async function count(options: PodcastSearchOptions, adaptParameters?: boolean): Promise<number> {
|
|
162
|
+
const result = await search({
|
|
163
|
+
...options,
|
|
164
|
+
size: 0
|
|
165
|
+
}, adaptParameters);
|
|
166
|
+
|
|
167
|
+
return result.count;
|
|
168
|
+
}
|
|
169
|
+
|
|
149
170
|
/**
|
|
150
171
|
* Retrieve the podcasts matching the search criterias, with all their data.
|
|
151
172
|
* This query is longer, because it also needs to retrieve organisations &
|
|
@@ -185,6 +206,7 @@ async function searchFull(options:PodcastSearchOptions, adaptParameters?: boolea
|
|
|
185
206
|
}
|
|
186
207
|
|
|
187
208
|
export const podcastApi = {
|
|
209
|
+
count,
|
|
188
210
|
downloadRegister,
|
|
189
211
|
get,
|
|
190
212
|
search,
|
|
@@ -150,7 +150,7 @@ export const useRights = () => {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
function canEditTranscript(): boolean {
|
|
153
|
-
return roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION');
|
|
153
|
+
return roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION', 'RESTRICTED_PRODUCTION');
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
function canSeeHistory(): boolean {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Podcast } from "@/stores/class/general/podcast";
|
|
2
|
+
import { Emission, SeasonMode } from "../../stores/class/general/emission";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Composable to facilitate seasons operations
|
|
6
|
+
*/
|
|
7
|
+
export const useSeasonsManagement = () => {
|
|
8
|
+
/**
|
|
9
|
+
* Indicates that seasons are enabled on the given emission
|
|
10
|
+
* @param emission The emission to check for seasons
|
|
11
|
+
* @returns True if seasons are enabled, false otherwise
|
|
12
|
+
*/
|
|
13
|
+
function areSeasonsEnabled(emission: Emission): boolean {
|
|
14
|
+
return [
|
|
15
|
+
SeasonMode.SEASON_WITHOUT_PODCAST_NUMBERING,
|
|
16
|
+
SeasonMode.SEASON_WITH_PODCAST_NUMBERING
|
|
17
|
+
].includes(emission.seasonMode);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Simple formatter to display season/episode of the given podcast
|
|
22
|
+
* @param podcast The podcast to check
|
|
23
|
+
* @returns A string describing the season/episode of the podcast or null
|
|
24
|
+
* if no seasons are defined
|
|
25
|
+
*/
|
|
26
|
+
function formatSeason(podcast: Podcast): string|null {
|
|
27
|
+
switch (podcast.emission.seasonMode) {
|
|
28
|
+
case SeasonMode.NO_SEASON:
|
|
29
|
+
return null;
|
|
30
|
+
|
|
31
|
+
case SeasonMode.SEASON_WITHOUT_PODCAST_NUMBERING:
|
|
32
|
+
return `S${podcast.seasonNumber}`;
|
|
33
|
+
|
|
34
|
+
case SeasonMode.SEASON_WITH_PODCAST_NUMBERING:
|
|
35
|
+
return `S${podcast.seasonNumber}·E${podcast.seasonEpisodeNumber}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
areSeasonsEnabled,
|
|
41
|
+
formatSeason
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -17,11 +17,10 @@
|
|
|
17
17
|
width="330"
|
|
18
18
|
height="330"
|
|
19
19
|
aria-hidden="true"
|
|
20
|
-
alt=""
|
|
21
|
-
|
|
22
20
|
:title="t('Emission name image', { name: emission.name })"
|
|
21
|
+
:alt="t('Emission name image', { name: emission.name })"
|
|
23
22
|
class="img-box"
|
|
24
|
-
|
|
23
|
+
>
|
|
25
24
|
</div>
|
|
26
25
|
<div class="fw-bold text-uppercase text-truncate p-2">
|
|
27
26
|
{{ emission.name }}
|
|
@@ -64,7 +63,10 @@
|
|
|
64
63
|
:podcast="p"
|
|
65
64
|
/>
|
|
66
65
|
</div>
|
|
67
|
-
<PodcastPlayBasicButton
|
|
66
|
+
<PodcastPlayBasicButton
|
|
67
|
+
v-if="!isProgressBar"
|
|
68
|
+
:podcast="p"
|
|
69
|
+
/>
|
|
68
70
|
</div>
|
|
69
71
|
</div>
|
|
70
72
|
<div
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
:label="t('Search')"
|
|
19
19
|
/>
|
|
20
20
|
</div>
|
|
21
|
+
|
|
21
22
|
<PodcastList
|
|
23
|
+
v-if="!showSeasons"
|
|
22
24
|
:first="dfirst"
|
|
23
25
|
:size="dsize"
|
|
24
26
|
:iab-id="iabId"
|
|
@@ -34,6 +36,36 @@
|
|
|
34
36
|
:force-update-parameters="forceUpdateParameters"
|
|
35
37
|
@fetch="fetch"
|
|
36
38
|
/>
|
|
39
|
+
<ClassicNav
|
|
40
|
+
v-else
|
|
41
|
+
v-model:active-tab="activeSeasonTab"
|
|
42
|
+
:tab-number="seasons.length"
|
|
43
|
+
>
|
|
44
|
+
<template v-for="season in seasons" #[tabNameSlot(season)]>
|
|
45
|
+
{{ $t('Podcast - Season N', { season }) }}
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<template v-for="season in seasons" #[tabContentSlot(season)] :key="season">
|
|
49
|
+
<PodcastList
|
|
50
|
+
class="flex-grow-1"
|
|
51
|
+
:first="dfirst"
|
|
52
|
+
:size="dsize"
|
|
53
|
+
:iab-id="iabId"
|
|
54
|
+
:query="query"
|
|
55
|
+
:participant-id="participantId"
|
|
56
|
+
:emission-id="emissionId"
|
|
57
|
+
:organisation-id="productorId"
|
|
58
|
+
:sort-criteria="sort"
|
|
59
|
+
:reload="reloadList"
|
|
60
|
+
:include-hidden="editRight"
|
|
61
|
+
:show-count="showCount"
|
|
62
|
+
:display-sort-text="false"
|
|
63
|
+
:force-update-parameters="forceUpdateParameters"
|
|
64
|
+
:seasons="[season]"
|
|
65
|
+
@fetch="fetch($event, season)"
|
|
66
|
+
/>
|
|
67
|
+
</template>
|
|
68
|
+
</ClassicNav>
|
|
37
69
|
</section>
|
|
38
70
|
</template>
|
|
39
71
|
|
|
@@ -44,28 +76,43 @@ import { Category } from "@/stores/class/general/category";
|
|
|
44
76
|
import { defineAsyncComponent, ref, Ref, computed, watch } from "vue";
|
|
45
77
|
import { Podcast } from "@/stores/class/general/podcast";
|
|
46
78
|
import { useI18n } from "vue-i18n";
|
|
79
|
+
import ClassicNav from "../../misc/ClassicNav.vue";
|
|
80
|
+
import { Emission } from "@/stores/class/general/emission";
|
|
81
|
+
import { useSeasonsManagement } from "../../composable/useSeasonsManagement";
|
|
82
|
+
import { PodcastSort } from "../../../api/podcastApi";
|
|
47
83
|
const CategoryChooser = defineAsyncComponent(
|
|
48
84
|
() => import("../categories/CategoryChooser.vue"),
|
|
49
85
|
);
|
|
50
86
|
|
|
51
87
|
//Props
|
|
52
|
-
const props = defineProps
|
|
53
|
-
first
|
|
54
|
-
size
|
|
55
|
-
query
|
|
56
|
-
participantId
|
|
57
|
-
name
|
|
58
|
-
emissionId
|
|
59
|
-
categoryFilter
|
|
60
|
-
reload
|
|
61
|
-
editRight
|
|
62
|
-
productorId
|
|
63
|
-
showCount
|
|
64
|
-
forceUpdateParameters
|
|
65
|
-
|
|
88
|
+
const props = withDefaults(defineProps<{
|
|
89
|
+
first?: number;
|
|
90
|
+
size?: number;
|
|
91
|
+
query?: string;
|
|
92
|
+
participantId?: number;
|
|
93
|
+
name?: string;
|
|
94
|
+
emissionId?: number;
|
|
95
|
+
categoryFilter?: boolean;
|
|
96
|
+
reload?: boolean;
|
|
97
|
+
editRight?: boolean;
|
|
98
|
+
productorId?: Array<string>;
|
|
99
|
+
showCount?: boolean;
|
|
100
|
+
forceUpdateParameters?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Emission for which to display podcasts
|
|
103
|
+
* If set, will check for seasons
|
|
104
|
+
*/
|
|
105
|
+
emission?: Emission;
|
|
106
|
+
}>(), {
|
|
107
|
+
first: 0,
|
|
108
|
+
size: 30
|
|
109
|
+
});
|
|
66
110
|
|
|
67
111
|
//Emits
|
|
68
|
-
const emit = defineEmits
|
|
112
|
+
const emit = defineEmits<{
|
|
113
|
+
(e: "fetch", podcasts: Array<Podcast>, season: number|undefined): void;
|
|
114
|
+
(e: "update:query", query: string): void;
|
|
115
|
+
}>();
|
|
69
116
|
|
|
70
117
|
//Data
|
|
71
118
|
const dfirst = ref(props.first);
|
|
@@ -76,6 +123,7 @@ const iabId : Ref<number | undefined>= ref(undefined);
|
|
|
76
123
|
|
|
77
124
|
//Composables
|
|
78
125
|
const { t } = useI18n();
|
|
126
|
+
const { areSeasonsEnabled } = useSeasonsManagement();
|
|
79
127
|
|
|
80
128
|
//Computed
|
|
81
129
|
const titleFilter = computed(() => {
|
|
@@ -84,7 +132,32 @@ const titleFilter = computed(() => {
|
|
|
84
132
|
: t("All podcast emission button");
|
|
85
133
|
});
|
|
86
134
|
const query = computed(() => searchPattern.value.length > 3 ? searchPattern.value : "");
|
|
87
|
-
|
|
135
|
+
|
|
136
|
+
const showSeasons = computed(() => {
|
|
137
|
+
return props.emission !== undefined && areSeasonsEnabled(props.emission) && props.emission.seasonCount > 0;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const activeSeasonTab = ref(showSeasons.value ? (props.emission?.seasonCount ?? 1) - 1 : 0);
|
|
141
|
+
|
|
142
|
+
const sort = computed((): PodcastSort => {
|
|
143
|
+
if(showSeasons.value === true) {
|
|
144
|
+
return PodcastSort.SEASONAL;
|
|
145
|
+
} else if(!query.value.length) {
|
|
146
|
+
return PodcastSort.DATE;
|
|
147
|
+
} else {
|
|
148
|
+
return PodcastSort.SCORE;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const seasons = computed((): Array<number> => {
|
|
153
|
+
const ary: Array<number> = [];
|
|
154
|
+
if (showSeasons.value === true) {
|
|
155
|
+
for (let i = 1; i <= props.emission.seasonCount; i++) {
|
|
156
|
+
ary.push(i);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return ary;
|
|
160
|
+
});
|
|
88
161
|
|
|
89
162
|
//Watch
|
|
90
163
|
watch(()=>props.reload, () => {
|
|
@@ -98,7 +171,16 @@ watch(searchPattern, () => {
|
|
|
98
171
|
function onCategorySelected(category: Category | undefined): void {
|
|
99
172
|
iabId.value = category?.id ? category.id : undefined;
|
|
100
173
|
}
|
|
101
|
-
function fetch(podcasts: Array<Podcast
|
|
102
|
-
emit("fetch", podcasts);
|
|
174
|
+
function fetch(podcasts: Array<Podcast>, season?: number): void {
|
|
175
|
+
emit("fetch", podcasts, season);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Name of the slot for the tab's title */
|
|
179
|
+
function tabNameSlot(season: number): string {
|
|
180
|
+
return `${season - 1}`;
|
|
181
|
+
}
|
|
182
|
+
/** Name of the slot for the tab's content */
|
|
183
|
+
function tabContentSlot(season: number): string {
|
|
184
|
+
return `tab${season - 1}`;
|
|
103
185
|
}
|
|
104
186
|
</script>
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
{{ title }}
|
|
6
6
|
</component>
|
|
7
7
|
</div>
|
|
8
|
+
|
|
8
9
|
<div v-if="!podcastId" class="d-flex justify-content-between">
|
|
9
|
-
<div class="d-flex">
|
|
10
|
+
<div v-if="!noSort" class="d-flex">
|
|
10
11
|
<button
|
|
11
12
|
class="btn btn-underline"
|
|
12
13
|
:class="{ active: !popularSort }"
|
|
@@ -82,6 +83,8 @@ const props = defineProps({
|
|
|
82
83
|
noRubriquageId: { default: () => [], type: Array as () => Array<number> },
|
|
83
84
|
podcastId: { default: undefined, type: Number },
|
|
84
85
|
titleTag: { default: "h2", type: String },
|
|
86
|
+
/** Hide sort options */
|
|
87
|
+
noSort: { default: false, type: Boolean }
|
|
85
88
|
})
|
|
86
89
|
|
|
87
90
|
//Emits
|
|
@@ -83,6 +83,7 @@ const props = withDefaults(defineProps<{
|
|
|
83
83
|
includeHidden?: boolean;
|
|
84
84
|
showCount?: boolean;
|
|
85
85
|
displaySortText?: boolean;
|
|
86
|
+
/** Criteria to sort on */
|
|
86
87
|
sortCriteria?: PodcastSort;
|
|
87
88
|
validity?: 'true'|'false'|''|boolean; // TODO improve this
|
|
88
89
|
rubriqueId?: Array<number>;
|
|
@@ -96,6 +97,8 @@ const props = withDefaults(defineProps<{
|
|
|
96
97
|
beneficiaries?: Array<string>;
|
|
97
98
|
/** The emission groups to filter on */
|
|
98
99
|
emissionGroups?: Array<EmissionGroup>;
|
|
100
|
+
/** The seasons to filter on */
|
|
101
|
+
seasons?: Array<number>;
|
|
99
102
|
}>(), {
|
|
100
103
|
first: 0,
|
|
101
104
|
size: 30,
|
|
@@ -223,7 +226,8 @@ async function fetchContent(reset: boolean): Promise<void> {
|
|
|
223
226
|
processingStatus: [PodcastProcessingStatus.Ready, PodcastProcessingStatus.Processing],
|
|
224
227
|
withVideo: props.withVideo,
|
|
225
228
|
tags: props.includeTag?.length ? props.includeTag : undefined,
|
|
226
|
-
beneficiaries: props.beneficiaries ?? undefined
|
|
229
|
+
beneficiaries: props.beneficiaries ?? undefined,
|
|
230
|
+
season: props.seasons
|
|
227
231
|
};
|
|
228
232
|
|
|
229
233
|
try {
|
|
@@ -88,6 +88,12 @@
|
|
|
88
88
|
{{ podcast.organisation.name }}
|
|
89
89
|
</router-link>
|
|
90
90
|
</div>
|
|
91
|
+
<div v-if="showSeasonNumber" class="mb-1">
|
|
92
|
+
{{ `${t('Podcast - Season')} : ${podcast.seasonNumber}` }}
|
|
93
|
+
</div>
|
|
94
|
+
<div v-if="showSeasonEpisodeNumber" class="mb-1">
|
|
95
|
+
{{ `${t('Podcast - Episode number')} : ${podcast.seasonEpisodeNumber}` }}
|
|
96
|
+
</div>
|
|
91
97
|
<div v-if="'' !== photoCredit" class="mb-1">
|
|
92
98
|
{{ t("Photo credits") + " : " + photoCredit }}
|
|
93
99
|
</div>
|
|
@@ -161,8 +167,12 @@ import { state } from "../../../stores/ParamSdkStore";
|
|
|
161
167
|
import { useAuthStore } from "../../../stores/AuthStore";
|
|
162
168
|
import displayHelper from "../../../helper/displayHelper";
|
|
163
169
|
import {usePodcastView} from "../../composable/podcasts/usePodcastView";
|
|
164
|
-
import { Podcast } from "
|
|
165
|
-
import { Conference } from "
|
|
170
|
+
import { Podcast } from "../../../stores/class/general/podcast";
|
|
171
|
+
import { Conference } from "../../../stores/class/conference/conference";
|
|
172
|
+
import { useI18n } from "vue-i18n";
|
|
173
|
+
import { useRouter } from "vue-router";
|
|
174
|
+
import { useSeasonsManagement } from "../../composable/useSeasonsManagement";
|
|
175
|
+
import { SeasonMode } from "../../../stores/class/general/emission";
|
|
166
176
|
|
|
167
177
|
import { defineAsyncComponent, toRefs, computed } from "vue";
|
|
168
178
|
const ErrorMessage = defineAsyncComponent(
|
|
@@ -193,8 +203,6 @@ const Countdown = defineAsyncComponent(() => import("../live/CountDown.vue"));
|
|
|
193
203
|
const TagList = defineAsyncComponent(() => import("./TagList.vue"));
|
|
194
204
|
const ShareAnonymous = defineAsyncComponent(() => import("../sharing/ShareAnonymous.vue"));
|
|
195
205
|
const PodcastRubriqueList = defineAsyncComponent(() => import("./PodcastRubriqueList.vue"));
|
|
196
|
-
import { useI18n } from "vue-i18n";
|
|
197
|
-
import { useRouter } from "vue-router";
|
|
198
206
|
|
|
199
207
|
//Props
|
|
200
208
|
const props = defineProps<{
|
|
@@ -222,6 +230,7 @@ const {
|
|
|
222
230
|
} = usePodcastView(propsRef.podcast, propsRef.podcastConference);
|
|
223
231
|
const authStore = useAuthStore();
|
|
224
232
|
const router = useRouter();
|
|
233
|
+
const { areSeasonsEnabled } = useSeasonsManagement();
|
|
225
234
|
|
|
226
235
|
//Computed
|
|
227
236
|
const podcastRubriques = computed(() => {
|
|
@@ -279,6 +288,14 @@ const tags = computed(() => {
|
|
|
279
288
|
return tags;
|
|
280
289
|
});
|
|
281
290
|
|
|
291
|
+
const showSeasonNumber = computed((): boolean => {
|
|
292
|
+
return areSeasonsEnabled(props.podcast.emission) && props.podcast.seasonNumber !== undefined;
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const showSeasonEpisodeNumber = computed((): boolean => {
|
|
296
|
+
return props.podcast.emission.seasonMode === SeasonMode.SEASON_WITH_PODCAST_NUMBERING && props.podcast.seasonEpisodeNumber !== undefined;
|
|
297
|
+
});
|
|
298
|
+
|
|
282
299
|
//Methods
|
|
283
300
|
function formatCredits(credits: string|undefined): string {
|
|
284
301
|
if (credits === undefined) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
:rubrique-id="rubriqueId"
|
|
12
12
|
:no-rubriquage-id="noRubriquageId"
|
|
13
13
|
:title-tag="titleTag"
|
|
14
|
+
:no-sort="noSort"
|
|
14
15
|
@sort-chrono="sortChrono"
|
|
15
16
|
@sort-popular="sortPopular"
|
|
16
17
|
>
|
|
@@ -59,10 +60,14 @@ const props = defineProps({
|
|
|
59
60
|
rubriquageId: { default: () => [], type: Array as () => Array<number> },
|
|
60
61
|
noRubriquageId: { default: () => [], type: Array as () => Array<number> },
|
|
61
62
|
query: { default: undefined, type: String },
|
|
63
|
+
/** Filter on season */
|
|
64
|
+
season: { default: undefined, type: Number },
|
|
62
65
|
lastThreeMonths: { default: false, type: Boolean },
|
|
63
66
|
titleTag: { default: "h2", type: String },
|
|
64
67
|
/** The podcast from which suggestions are made */
|
|
65
|
-
podcastId: { type: Number }
|
|
68
|
+
podcastId: { type: Number },
|
|
69
|
+
/** Hide sort options */
|
|
70
|
+
noSort: { default: false, type: Boolean }
|
|
66
71
|
})
|
|
67
72
|
|
|
68
73
|
//Emits
|
|
@@ -104,6 +109,7 @@ onBeforeMount(()=>{
|
|
|
104
109
|
|
|
105
110
|
//Methods
|
|
106
111
|
async function fetchNext(): Promise<void> {
|
|
112
|
+
// TODO use podcastApi
|
|
107
113
|
const data = await classicApi.fetchData<ListClassicReturn<Podcast>>({
|
|
108
114
|
api: 0,
|
|
109
115
|
path: "podcast/search",
|
|
@@ -123,10 +129,10 @@ async function fetchNext(): Promise<void> {
|
|
|
123
129
|
sort: popularSort.value ? "POPULARITY" : "DATE",
|
|
124
130
|
query: props.query,
|
|
125
131
|
includeStatus: ["READY", "PROCESSING"],
|
|
126
|
-
after:
|
|
127
|
-
popularSort.value && props.lastThreeMonths
|
|
132
|
+
after: popularSort.value && props.lastThreeMonths
|
|
128
133
|
? dayjs().subtract(3, "months").toISOString()
|
|
129
134
|
: undefined,
|
|
135
|
+
season: props.season
|
|
130
136
|
},
|
|
131
137
|
specialTreatement: true,
|
|
132
138
|
});
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
`selected` : true if the option is selected
|
|
11
11
|
-->
|
|
12
12
|
<template>
|
|
13
|
-
<div role="radiogroup" class="d-flex" :class="isColumn ? 'flex-column' : ''">
|
|
13
|
+
<div role="radiogroup" class="d-flex" :class="isColumn !== false ? 'flex-column' : ''">
|
|
14
14
|
<div
|
|
15
15
|
v-for="option in options"
|
|
16
16
|
:key="option.title"
|
|
17
17
|
class="octopus-form-item"
|
|
18
|
-
:class="isColumn ? 'd-flex flex-nowrap align-items-center' : 'me-2'"
|
|
18
|
+
:class="isColumn !== false ? 'd-flex flex-nowrap align-items-center' : 'me-2'"
|
|
19
19
|
>
|
|
20
20
|
<input
|
|
21
21
|
:id="idRadio + option.value"
|
|
@@ -37,22 +37,21 @@
|
|
|
37
37
|
|
|
38
38
|
<script setup generic="T extends { title: string; value: string|undefined; }" lang="ts">
|
|
39
39
|
//Props
|
|
40
|
-
const { textInit } = defineProps
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
textInit: { default: undefined, type: String },
|
|
48
|
-
isColumn: { default: true, type: Boolean },
|
|
49
|
-
})
|
|
40
|
+
const { textInit, isColumn = true } = defineProps<{
|
|
41
|
+
options: Array<T>;
|
|
42
|
+
textInit?: string;
|
|
43
|
+
idRadio?: string;
|
|
44
|
+
isDisabled?: boolean;
|
|
45
|
+
isColumn?: boolean;
|
|
46
|
+
}>();
|
|
50
47
|
|
|
51
48
|
//Emits
|
|
52
|
-
const emit = defineEmits
|
|
49
|
+
const emit = defineEmits<{
|
|
50
|
+
(e: 'update:textInit', value: string): void;
|
|
51
|
+
}>();
|
|
53
52
|
|
|
54
53
|
//Methods
|
|
55
|
-
function onChange(value:string){
|
|
54
|
+
function onChange(value: string){
|
|
56
55
|
emit('update:textInit', value)
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
:options="options?.topBarMainContent"
|
|
15
15
|
/>
|
|
16
16
|
</header>
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
<div
|
|
19
|
+
v-if="generalStore.contentToDisplay"
|
|
20
|
+
class="header-content-bg"
|
|
21
|
+
:style="headerBackgroundImage"
|
|
22
|
+
:class="{ scrolled: scrolled, 'header-force-blur':needToBlur }"
|
|
23
|
+
>
|
|
18
24
|
<div class="header-additional-content header-content">
|
|
19
25
|
<h1 v-if="!scrolled" class="text-truncate">
|
|
20
26
|
{{ titleToDisplay }}
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
:just-buttons="true"
|
|
55
55
|
/>
|
|
56
56
|
<div class="ms-2 fw-bold">
|
|
57
|
-
{{
|
|
57
|
+
{{ messageListenEpisode }}
|
|
58
58
|
</div>
|
|
59
59
|
</div>
|
|
60
60
|
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
:size="ps"
|
|
100
100
|
:show-count="true"
|
|
101
101
|
:emission-id="emissionId"
|
|
102
|
+
:emission="emission"
|
|
102
103
|
:category-filter="false"
|
|
103
104
|
:edit-right="editRight"
|
|
104
105
|
:productor-id="[emission.orga.id]"
|
|
@@ -138,6 +139,7 @@ import { useSimplePageParam } from "../composable/route/useSimplePageParam";
|
|
|
138
139
|
import ErrorMessage from "../misc/ErrorMessage.vue";
|
|
139
140
|
import ClassicHelpButton from "../misc/ClassicHelpButton.vue";
|
|
140
141
|
import { emissionApi } from "../../api/emissionApi";
|
|
142
|
+
import { useSeasonsManagement } from "../composable/useSeasonsManagement";
|
|
141
143
|
|
|
142
144
|
const ShareAnonymous = defineAsyncComponent(() => import("../display/sharing/ShareAnonymous.vue"));
|
|
143
145
|
const PodcastFilterList = defineAsyncComponent(
|
|
@@ -193,15 +195,16 @@ const { t } = useI18n();
|
|
|
193
195
|
const { useProxyImageUrl } = useImageProxy();
|
|
194
196
|
const { isPodcastmaker, isEditRights, authOrgaId } = useOrgaComputed();
|
|
195
197
|
const { updatePathParams } = useSeoTitleUrl();
|
|
196
|
-
const {handle403} = useErrorHandler();
|
|
198
|
+
const { handle403 } = useErrorHandler();
|
|
197
199
|
const filterStore = useFilterStore();
|
|
198
|
-
const generalStore= useGeneralStore();
|
|
199
|
-
const route= useRoute();
|
|
200
|
+
const generalStore = useGeneralStore();
|
|
201
|
+
const route = useRoute();
|
|
200
202
|
const {
|
|
201
203
|
searchPattern,
|
|
202
204
|
paginateFirst,
|
|
203
205
|
isInit
|
|
204
206
|
} = useSimplePageParam(props, true);
|
|
207
|
+
const { areSeasonsEnabled, formatSeason } = useSeasonsManagement();
|
|
205
208
|
|
|
206
209
|
|
|
207
210
|
//Computed
|
|
@@ -209,9 +212,17 @@ const name = computed(() => emission.value?.name ?? "");
|
|
|
209
212
|
const description = computed(() => emission.value?.description ?? "");
|
|
210
213
|
const editRight = computed(() => isEditRights(emission.value?.orga.id));
|
|
211
214
|
|
|
215
|
+
const messageListenEpisode = computed((): string => {
|
|
216
|
+
const base = t("Listen to the latest episode");
|
|
217
|
+
if (lastPodcast.value !== undefined && areSeasonsEnabled(emission.value)) {
|
|
218
|
+
return base + ` (${formatSeason(lastPodcast.value)})`;
|
|
219
|
+
} else {
|
|
220
|
+
return base;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
212
223
|
|
|
213
224
|
//Watch
|
|
214
|
-
watch(()=>props.emissionId,
|
|
225
|
+
watch(() => props.emissionId, getEmissionDetails, { immediate: true });
|
|
215
226
|
|
|
216
227
|
|
|
217
228
|
onBeforeUnmount(() => {
|
|
@@ -248,12 +259,13 @@ async function getEmissionDetails(): Promise<void> {
|
|
|
248
259
|
initError();
|
|
249
260
|
}
|
|
250
261
|
}
|
|
251
|
-
function podcastsFetched(podcasts: Array<Podcast
|
|
262
|
+
function podcastsFetched(podcasts: Array<Podcast>, season: number|undefined) {
|
|
263
|
+
if (season !== undefined && season !== emission.value?.seasonCount) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
252
267
|
for (const podcast of podcasts) {
|
|
253
|
-
if (
|
|
254
|
-
"READY" === podcast.processingStatus &&
|
|
255
|
-
podcast.availability.visibility
|
|
256
|
-
) {
|
|
268
|
+
if ("READY" === podcast.processingStatus && podcast.availability.visibility) {
|
|
257
269
|
lastPodcast.value = podcast;
|
|
258
270
|
return;
|
|
259
271
|
}
|
package/src/locale/de.json
CHANGED
|
@@ -417,5 +417,11 @@
|
|
|
417
417
|
"Podcast": "Folge",
|
|
418
418
|
"Player - Transcription - AI Warning": "Die Transkription basiert auf KI und kann Fehler enthalten, bitte teilen Sie uns dies mit.",
|
|
419
419
|
"All organisations": "Alle Organisationen",
|
|
420
|
-
"Emission subtitle": "Untertitel der Sendung"
|
|
420
|
+
"Emission subtitle": "Untertitel der Sendung",
|
|
421
|
+
"Podcast type - Full": "Standard",
|
|
422
|
+
"Podcast type - Trailer": "Anhänger",
|
|
423
|
+
"Podcast type - Bonus": "Boni",
|
|
424
|
+
"Podcast - Season N": "Saison {season}",
|
|
425
|
+
"Podcast - Season": "Jahreszeit",
|
|
426
|
+
"Podcast - Episode number": "Episodennummer"
|
|
421
427
|
}
|
package/src/locale/en.json
CHANGED
|
@@ -417,5 +417,11 @@
|
|
|
417
417
|
"Podcast": "Episode",
|
|
418
418
|
"Player - Transcription - AI Warning": "The transcription is based on AI and may contain errors, please let us know.",
|
|
419
419
|
"All organisations": "All organizations",
|
|
420
|
-
"Emission subtitle": "Subtitle of the show"
|
|
420
|
+
"Emission subtitle": "Subtitle of the show",
|
|
421
|
+
"Podcast type - Full": "Standard",
|
|
422
|
+
"Podcast type - Trailer": "Trailer",
|
|
423
|
+
"Podcast type - Bonus": "Bonuses",
|
|
424
|
+
"Podcast - Season N": "Saison {season}",
|
|
425
|
+
"Podcast - Season": "Season",
|
|
426
|
+
"Podcast - Episode number": "Episode number"
|
|
421
427
|
}
|
package/src/locale/es.json
CHANGED
|
@@ -417,5 +417,11 @@
|
|
|
417
417
|
"Podcast": "Episodio",
|
|
418
418
|
"Player - Transcription - AI Warning": "La transcripción se basa en IA y puede contener errores, háganoslo saber.",
|
|
419
419
|
"All organisations": "Todas las organizaciones",
|
|
420
|
-
"Emission subtitle": "Subtítulo del programa"
|
|
420
|
+
"Emission subtitle": "Subtítulo del programa",
|
|
421
|
+
"Podcast type - Full": "Estándar",
|
|
422
|
+
"Podcast type - Trailer": "Tráiler",
|
|
423
|
+
"Podcast type - Bonus": "Bonificaciones",
|
|
424
|
+
"Podcast - Season N": "Temporada {season}",
|
|
425
|
+
"Podcast - Season": "Estación",
|
|
426
|
+
"Podcast - Episode number": "Número de episodio"
|
|
421
427
|
}
|
package/src/locale/fr.json
CHANGED
|
@@ -440,5 +440,11 @@
|
|
|
440
440
|
"SmartLink - To emission on podcastmaker": "Page officielle de l'émission - {organisation}",
|
|
441
441
|
"SmartLink - To playlist on podcastmaker": "Page officielle de la playlist - {organisation}",
|
|
442
442
|
"Miniplayer - Parameters - Auto height": "Hauteur automatique",
|
|
443
|
-
"Player - Transcription - AI Warning": "La transcription est basée sur une IA et peut comporter des erreurs, n’hésitez pas à nous en informer."
|
|
443
|
+
"Player - Transcription - AI Warning": "La transcription est basée sur une IA et peut comporter des erreurs, n’hésitez pas à nous en informer.",
|
|
444
|
+
"Podcast type - Full": "Standard",
|
|
445
|
+
"Podcast type - Trailer": "Trailer",
|
|
446
|
+
"Podcast type - Bonus": "Bonus",
|
|
447
|
+
"Podcast - Season N": "Saison {season}",
|
|
448
|
+
"Podcast - Season": "Saison",
|
|
449
|
+
"Podcast - Episode number": "Numéro d'épisode"
|
|
444
450
|
}
|
package/src/locale/it.json
CHANGED
|
@@ -418,5 +418,11 @@
|
|
|
418
418
|
"Podcast": "Episodio",
|
|
419
419
|
"Player - Transcription - AI Warning": "La trascrizione si basa sull'intelligenza artificiale e potrebbe contenere errori, faccelo sapere.",
|
|
420
420
|
"All organisations": "Tutte le organizzazioni",
|
|
421
|
-
"Emission subtitle": "Sottotitolo dello spettacolo"
|
|
421
|
+
"Emission subtitle": "Sottotitolo dello spettacolo",
|
|
422
|
+
"Podcast type - Full": "Standard",
|
|
423
|
+
"Podcast type - Trailer": "Rimorchio",
|
|
424
|
+
"Podcast type - Bonus": "Bonus",
|
|
425
|
+
"Podcast - Season N": "Stagione {season}",
|
|
426
|
+
"Podcast - Season": "Stagione",
|
|
427
|
+
"Podcast - Episode number": "Numero dell'episodio"
|
|
422
428
|
}
|
package/src/locale/sl.json
CHANGED
|
@@ -416,5 +416,11 @@
|
|
|
416
416
|
"Podcast": "Epizoda",
|
|
417
417
|
"Player - Transcription - AI Warning": "Transkripcija temelji na umetni inteligenci in lahko vsebuje napake, zato nas obvestite.",
|
|
418
418
|
"All organisations": "Vse organizacije",
|
|
419
|
-
"Emission subtitle": "Podnaslov oddaje"
|
|
419
|
+
"Emission subtitle": "Podnaslov oddaje",
|
|
420
|
+
"Podcast type - Full": "Standardno",
|
|
421
|
+
"Podcast type - Trailer": "Napovednik",
|
|
422
|
+
"Podcast type - Bonus": "Bonusi",
|
|
423
|
+
"Podcast - Season N": "Sezona {season}",
|
|
424
|
+
"Podcast - Season": "Sezona",
|
|
425
|
+
"Podcast - Episode number": "Številka epizode"
|
|
420
426
|
}
|
|
@@ -4,6 +4,18 @@ import { Person } from "../user/person";
|
|
|
4
4
|
import { ItuneCategory } from "./ituneCategory";
|
|
5
5
|
import { Annotations } from ".";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Season settings for emissions
|
|
9
|
+
*/
|
|
10
|
+
export enum SeasonMode {
|
|
11
|
+
/** This emission doesn't have seasons */
|
|
12
|
+
NO_SEASON = 'NO_SEASON',
|
|
13
|
+
/** This emission has seasons and episodes are ordered by date */
|
|
14
|
+
SEASON_WITHOUT_PODCAST_NUMBERING = 'SEASON_WITHOUT_PODCAST_NUMBERING',
|
|
15
|
+
/** This emission has seasons and episodes are ordered manually */
|
|
16
|
+
SEASON_WITH_PODCAST_NUMBERING = 'SEASON_WITH_PODCAST_NUMBERING'
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
/**
|
|
8
20
|
* An emission
|
|
9
21
|
*/
|
|
@@ -40,6 +52,12 @@ export interface Emission {
|
|
|
40
52
|
tags?: string[];
|
|
41
53
|
/** The ids of groups this emission belongs to */
|
|
42
54
|
groupIds?: Array<number>
|
|
55
|
+
/** Seasons configuration */
|
|
56
|
+
seasonMode: SeasonMode;
|
|
57
|
+
/** Number of seasons on this emission */
|
|
58
|
+
seasonCount: number;
|
|
59
|
+
/** Indicates that the emission has explicit content */
|
|
60
|
+
explicit?: boolean;
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
export function emptyEmissionData(orga?: Organisation): Emission {
|
|
@@ -54,5 +72,7 @@ export function emptyEmissionData(orga?: Organisation): Emission {
|
|
|
54
72
|
rubriqueIds: [],
|
|
55
73
|
monetisable: "UNDEFINED",
|
|
56
74
|
limits: {},
|
|
75
|
+
seasonMode: SeasonMode.NO_SEASON,
|
|
76
|
+
seasonCount: 0
|
|
57
77
|
};
|
|
58
78
|
}
|
|
@@ -17,6 +17,13 @@ export enum PodcastProcessingStatus {
|
|
|
17
17
|
All = "ALL"
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Type of episode */
|
|
21
|
+
export enum PodcastType {
|
|
22
|
+
FULL = 'full',
|
|
23
|
+
TRAILER = 'trailer',
|
|
24
|
+
BONUS = 'bonus'
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
/** Describe the availability of the podcast */
|
|
21
28
|
export interface PodcastAvailability {
|
|
22
29
|
date?: number | null;
|
|
@@ -46,6 +53,8 @@ export interface Podcast {
|
|
|
46
53
|
availability: PodcastAvailability;
|
|
47
54
|
/** Description of the podcast */
|
|
48
55
|
description?: string;
|
|
56
|
+
/** Optional summary of the podcast */
|
|
57
|
+
summary?: string;
|
|
49
58
|
/** Publishing date */
|
|
50
59
|
pubDate?: string;
|
|
51
60
|
/** The status of the processing of the audio file */
|
|
@@ -56,6 +65,14 @@ export interface Podcast {
|
|
|
56
65
|
ofTags?: Array<string>;
|
|
57
66
|
/** List of beneficiaries/rights holders */
|
|
58
67
|
beneficiaries: Array<string>;
|
|
68
|
+
/** Indicates that the episode has explicit content */
|
|
69
|
+
explicit?: boolean;
|
|
70
|
+
/** Type of episode */
|
|
71
|
+
seasonEpisodeType: PodcastType;
|
|
72
|
+
/** Number of season in which podcast belongs */
|
|
73
|
+
seasonNumber?: number;
|
|
74
|
+
/** Order of the episode in the season */
|
|
75
|
+
seasonEpisodeNumber?: number;
|
|
59
76
|
|
|
60
77
|
createdAt?: string;
|
|
61
78
|
createdByUserId?: string;
|
package/src/style/general.scss
CHANGED
|
@@ -47,6 +47,13 @@ main, #app{
|
|
|
47
47
|
font-family: var(--octopus-font-family);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Display an asterisk for required fields
|
|
51
|
+
label.required:after {
|
|
52
|
+
content: "*";
|
|
53
|
+
font-weight: bold;
|
|
54
|
+
margin-left: 3px;
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
/** Link style */
|
|
51
58
|
a{
|
|
52
59
|
word-break: break-word;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('../../src/api/classicApi', () => ({
|
|
4
|
+
default: {
|
|
5
|
+
fetchData: vi.fn()
|
|
6
|
+
}
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import classicApi from '../../src/api/classicApi';
|
|
10
|
+
import { podcastApi } from '../../src/api/podcastApi';
|
|
11
|
+
|
|
12
|
+
const baseOptions = { organisationId: ['org-1'] };
|
|
13
|
+
|
|
14
|
+
describe('podcastApi', () => {
|
|
15
|
+
describe('count', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.mocked(classicApi.fetchData).mockResolvedValue({ count: 42, result: [], sort: null });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns the count from search results', async () => {
|
|
21
|
+
const result = await podcastApi.count(baseOptions);
|
|
22
|
+
expect(result).toBe(42);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('calls search with size 0', async () => {
|
|
26
|
+
await podcastApi.count(baseOptions);
|
|
27
|
+
expect(classicApi.fetchData).toHaveBeenCalledWith(
|
|
28
|
+
expect.objectContaining({
|
|
29
|
+
parameters: expect.objectContaining({ size: 0 })
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('forwards other options to search', async () => {
|
|
35
|
+
await podcastApi.count({ ...baseOptions, emissionId: 5 });
|
|
36
|
+
expect(classicApi.fetchData).toHaveBeenCalledWith(
|
|
37
|
+
expect.objectContaining({
|
|
38
|
+
parameters: expect.objectContaining({ organisationId: ['org-1'], emissionId: 5 })
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { useSeasonsManagement } from '@/components/composable/useSeasonsManagement';
|
|
3
|
+
import { emptyEmissionData, SeasonMode } from '@/stores/class/general/emission';
|
|
4
|
+
import { emptyPodcastData } from '@/stores/class/general/podcast';
|
|
5
|
+
|
|
6
|
+
const { areSeasonsEnabled, formatSeason } = useSeasonsManagement();
|
|
7
|
+
|
|
8
|
+
describe('useSeasonsManagement', () => {
|
|
9
|
+
describe('areSeasonsEnabled', () => {
|
|
10
|
+
it.each([
|
|
11
|
+
SeasonMode.SEASON_WITH_PODCAST_NUMBERING,
|
|
12
|
+
SeasonMode.SEASON_WITHOUT_PODCAST_NUMBERING,
|
|
13
|
+
])('returns true for %s', (seasonMode) => {
|
|
14
|
+
expect(areSeasonsEnabled({ ...emptyEmissionData(), seasonMode })).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns false for NO_SEASON', () => {
|
|
18
|
+
expect(areSeasonsEnabled(emptyEmissionData())).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('formatSeason', () => {
|
|
23
|
+
it.each([
|
|
24
|
+
{ seasonMode: SeasonMode.NO_SEASON, seasonNumber: 1, seasonEpisodeNumber: 1, expected: null },
|
|
25
|
+
{ seasonMode: SeasonMode.SEASON_WITHOUT_PODCAST_NUMBERING, seasonNumber: 2, seasonEpisodeNumber: 1, expected: 'S2' },
|
|
26
|
+
{ seasonMode: SeasonMode.SEASON_WITH_PODCAST_NUMBERING, seasonNumber: 2, seasonEpisodeNumber: 5, expected: 'S2·E5' },
|
|
27
|
+
])('formats $seasonMode as $expected', ({ seasonMode, seasonNumber, seasonEpisodeNumber, expected }) => {
|
|
28
|
+
const podcast = emptyPodcastData();
|
|
29
|
+
podcast.seasonNumber = seasonNumber;
|
|
30
|
+
podcast.seasonEpisodeNumber = seasonEpisodeNumber;
|
|
31
|
+
podcast.emission.seasonMode = seasonMode;
|
|
32
|
+
expect(formatSeason(podcast)).toBe(expected);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import '@tests/mocks/i18n';
|
|
2
|
+
|
|
3
|
+
import PodcastFilterList from '@/components/display/podcasts/PodcastFilterList.vue';
|
|
4
|
+
import { emptyEmissionData, SeasonMode } from '@/stores/class/general/emission';
|
|
5
|
+
import { mount as testMount } from '@tests/utils';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const mount = (props: Record<string, unknown> = {}) =>
|
|
9
|
+
testMount(PodcastFilterList, { shallow: true, props });
|
|
10
|
+
|
|
11
|
+
describe('PodcastFilterList', () => {
|
|
12
|
+
describe('season display', () => {
|
|
13
|
+
it.each([
|
|
14
|
+
{ desc: 'no emission provided', props: {} },
|
|
15
|
+
{ desc: 'emission has NO_SEASON mode', props: { emission: { ...emptyEmissionData(), seasonMode: SeasonMode.NO_SEASON, seasonCount: 3 } } },
|
|
16
|
+
{ desc: 'emission has seasons but seasonCount is 0', props: { emission: { ...emptyEmissionData(), seasonMode: SeasonMode.SEASON_WITH_PODCAST_NUMBERING, seasonCount: 0 } } },
|
|
17
|
+
])('shows a plain list when $desc', async ({ props }) => {
|
|
18
|
+
const wrapper = await mount(props);
|
|
19
|
+
expect(wrapper.find('podcast-list-stub').exists()).toBe(true);
|
|
20
|
+
expect(wrapper.find('classic-nav-stub').exists()).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it.each([
|
|
24
|
+
SeasonMode.SEASON_WITH_PODCAST_NUMBERING,
|
|
25
|
+
SeasonMode.SEASON_WITHOUT_PODCAST_NUMBERING,
|
|
26
|
+
])('shows a season nav when seasonMode is %s', async (seasonMode) => {
|
|
27
|
+
const emission = { ...emptyEmissionData(), seasonMode, seasonCount: 3 };
|
|
28
|
+
const wrapper = await mount({ emission });
|
|
29
|
+
expect(wrapper.find('classic-nav-stub').exists()).toBe(true);
|
|
30
|
+
expect(wrapper.find('podcast-list-stub').exists()).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import '@tests/mocks/i18n';
|
|
2
|
+
import '@tests/mocks/useRouter';
|
|
3
|
+
|
|
4
|
+
import PodcastInlineListTemplate from '@/components/display/podcasts/PodcastInlineListTemplate.vue';
|
|
5
|
+
import { mount as testMount } from '@tests/utils';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const mount = (props: Record<string, unknown> = {}) =>
|
|
9
|
+
testMount(PodcastInlineListTemplate, { shallow: true, props });
|
|
10
|
+
|
|
11
|
+
describe('PodcastInlineListTemplate', () => {
|
|
12
|
+
describe('noSort prop', () => {
|
|
13
|
+
it('shows sort buttons by default', async () => {
|
|
14
|
+
const wrapper = await mount();
|
|
15
|
+
expect(wrapper.find('.btn-underline').exists()).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('hides sort buttons when noSort is true', async () => {
|
|
19
|
+
const wrapper = await mount({ noSort: true });
|
|
20
|
+
expect(wrapper.find('.btn-underline').exists()).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -2,47 +2,74 @@ import '@tests/mocks/i18n';
|
|
|
2
2
|
import '@tests/mocks/useRouter';
|
|
3
3
|
|
|
4
4
|
import PodcastModuleBox from '@/components/display/podcasts/PodcastModuleBox.vue';
|
|
5
|
+
import { SeasonMode } from '@/stores/class/general/emission';
|
|
5
6
|
import { emptyPodcastData, Podcast } from '@/stores/class/general/podcast';
|
|
6
|
-
import { mount, setupAuthStore } from '@tests/utils';
|
|
7
|
+
import { mount as testMount, setupAuthStore } from '@tests/utils';
|
|
7
8
|
import { describe, expect, it } from 'vitest';
|
|
8
9
|
import { initialize } from '@/stores/ParamSdkStore';
|
|
9
10
|
|
|
11
|
+
const mount = (podcast: Podcast) => testMount(PodcastModuleBox, {
|
|
12
|
+
props: { podcast },
|
|
13
|
+
stubs: ['ShareAnonymous', 'LikeSection'],
|
|
14
|
+
beforeMount: setupAuthStore()
|
|
15
|
+
});
|
|
16
|
+
|
|
10
17
|
describe('PodcastModuleBox', () => {
|
|
11
18
|
describe('date display', () => {
|
|
12
19
|
const podcast: Podcast = emptyPodcastData();
|
|
13
20
|
podcast.pubDate = '2025-12-01T10:21:31.000+00:00';
|
|
14
21
|
|
|
15
22
|
it('shows the date without time by default', async() => {
|
|
16
|
-
const wrapper = await mount(
|
|
17
|
-
props: { podcast },
|
|
18
|
-
stubs: ['ShareAnonymous', 'LikeSection'],
|
|
19
|
-
beforeMount: setupAuthStore()
|
|
20
|
-
});
|
|
23
|
+
const wrapper = await mount(podcast);
|
|
21
24
|
expect(wrapper.text()).toContain('1 December 2025');
|
|
22
25
|
expect(wrapper.text()).not.toContain('11:21');
|
|
23
26
|
});
|
|
24
27
|
|
|
25
|
-
it('shows the date
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
stubs: ['ShareAnonymous', 'LikeSection'],
|
|
29
|
-
beforeMount: setupAuthStore()
|
|
30
|
-
});
|
|
28
|
+
it('shows the date with time when enabled in SdkParams', async() => {
|
|
29
|
+
initialize({ generalParameters: { showTimeWithDates: true } });
|
|
30
|
+
const wrapper = await mount(podcast);
|
|
31
31
|
expect(wrapper.text()).toContain('1 December 2025');
|
|
32
|
-
expect(wrapper.text()).
|
|
32
|
+
expect(wrapper.text()).toContain('11:21');
|
|
33
33
|
});
|
|
34
|
+
});
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
describe('season info', () => {
|
|
37
|
+
function makePodcast(seasonMode = SeasonMode.NO_SEASON, overrides: Partial<Podcast> = {}) {
|
|
38
|
+
const podcast = emptyPodcastData();
|
|
39
|
+
podcast.emission.seasonMode = seasonMode;
|
|
40
|
+
return { ...podcast, ...overrides };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('season number', () => {
|
|
44
|
+
it('hidden when seasonNumber is not set', async () => {
|
|
45
|
+
const wrapper = await mount(makePodcast(SeasonMode.SEASON_WITH_PODCAST_NUMBERING));
|
|
46
|
+
expect(wrapper.text()).not.toContain('Podcast - Season');
|
|
38
47
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
|
|
49
|
+
it('hidden when emission has no season mode', async () => {
|
|
50
|
+
const wrapper = await mount(makePodcast(SeasonMode.NO_SEASON, { seasonNumber: 2 }));
|
|
51
|
+
expect(wrapper.text()).not.toContain('Podcast - Season');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it.each([
|
|
55
|
+
SeasonMode.SEASON_WITH_PODCAST_NUMBERING,
|
|
56
|
+
SeasonMode.SEASON_WITHOUT_PODCAST_NUMBERING,
|
|
57
|
+
])('shown for %s', async (seasonMode) => {
|
|
58
|
+
const wrapper = await mount(makePodcast(seasonMode, { seasonNumber: 2 }));
|
|
59
|
+
expect(wrapper.text()).toContain('Podcast - Season : 2');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('episode number', () => {
|
|
64
|
+
it('hidden for SEASON_WITHOUT_PODCAST_NUMBERING', async () => {
|
|
65
|
+
const wrapper = await mount(makePodcast(SeasonMode.SEASON_WITHOUT_PODCAST_NUMBERING, { seasonEpisodeNumber: 5 }));
|
|
66
|
+
expect(wrapper.text()).not.toContain('Podcast - Episode number');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('shown for SEASON_WITH_PODCAST_NUMBERING', async () => {
|
|
70
|
+
const wrapper = await mount(makePodcast(SeasonMode.SEASON_WITH_PODCAST_NUMBERING, { seasonEpisodeNumber: 5 }));
|
|
71
|
+
expect(wrapper.text()).toContain('Podcast - Episode number : 5');
|
|
43
72
|
});
|
|
44
|
-
expect(wrapper.text()).toContain('1 December 2025');
|
|
45
|
-
expect(wrapper.text()).toContain('11:21');
|
|
46
73
|
});
|
|
47
74
|
});
|
|
48
75
|
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import '@tests/mocks/i18n';
|
|
2
|
+
import '@tests/mocks/useRouter';
|
|
3
|
+
|
|
4
|
+
import EmissionPage from '@/components/pages/EmissionPage.vue';
|
|
5
|
+
import { emptyEmissionData, SeasonMode } from '@/stores/class/general/emission';
|
|
6
|
+
import { emptyPodcastData, PodcastProcessingStatus } from '@/stores/class/general/podcast';
|
|
7
|
+
import { mount, VueWrapper } from '@tests/utils';
|
|
8
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import { nextTick } from 'vue';
|
|
10
|
+
|
|
11
|
+
vi.mock('@/api/emissionApi', () => ({
|
|
12
|
+
emissionApi: { get: vi.fn() }
|
|
13
|
+
}));
|
|
14
|
+
vi.mock('@/components/composable/route/useSeoTitleUrl.ts', () => ({
|
|
15
|
+
useSeoTitleUrl: () => ({ updatePathParams: vi.fn() })
|
|
16
|
+
}));
|
|
17
|
+
vi.mock('@/components/composable/useImageProxy', () => ({
|
|
18
|
+
useImageProxy: () => ({ useProxyImageUrl: vi.fn() })
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import { emissionApi } from '@/api/emissionApi';
|
|
22
|
+
|
|
23
|
+
const publicOrga = { id: 'org-1', name: 'Test', imageUrl: '', privacy: 'PUBLIC' };
|
|
24
|
+
|
|
25
|
+
function makeEmission(seasonMode: SeasonMode) {
|
|
26
|
+
return { ...emptyEmissionData(), seasonMode, seasonCount: 2, orga: publicOrga };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeReadyPodcast(seasonMode: SeasonMode = SeasonMode.NO_SEASON) {
|
|
30
|
+
const podcast = emptyPodcastData();
|
|
31
|
+
podcast.processingStatus = PodcastProcessingStatus.Ready;
|
|
32
|
+
podcast.seasonNumber = 1;
|
|
33
|
+
podcast.seasonEpisodeNumber = 3;
|
|
34
|
+
podcast.emission.seasonMode = seasonMode;
|
|
35
|
+
return podcast;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function mountPage(seasonMode: SeasonMode) {
|
|
39
|
+
vi.mocked(emissionApi.get).mockResolvedValue(makeEmission(seasonMode));
|
|
40
|
+
return mount(EmissionPage, { shallow: true, props: { emissionId: 1 } });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function triggerFetch(wrapper: VueWrapper, seasonMode: SeasonMode, season?: number) {
|
|
44
|
+
await wrapper.findComponent({ name: 'PodcastFilterList' }).vm.$emit('fetch', [makeReadyPodcast(seasonMode)], season);
|
|
45
|
+
await nextTick();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('EmissionPage', () => {
|
|
49
|
+
describe('messageListenEpisode', () => {
|
|
50
|
+
it('does not show season info before any podcast is fetched', async () => {
|
|
51
|
+
const wrapper = await mountPage(SeasonMode.SEASON_WITH_PODCAST_NUMBERING);
|
|
52
|
+
expect(wrapper.text()).not.toContain('S1·E3');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('shows base message without season info for NO_SEASON emission', async () => {
|
|
56
|
+
const wrapper = await mountPage(SeasonMode.NO_SEASON);
|
|
57
|
+
await triggerFetch(wrapper, SeasonMode.NO_SEASON);
|
|
58
|
+
expect(wrapper.text()).toContain('Listen to the latest episode');
|
|
59
|
+
expect(wrapper.text()).not.toContain('(S');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it.each([
|
|
63
|
+
{ seasonMode: SeasonMode.SEASON_WITH_PODCAST_NUMBERING, expected: 'Listen to the latest episode (S1·E3)' },
|
|
64
|
+
{ seasonMode: SeasonMode.SEASON_WITHOUT_PODCAST_NUMBERING, expected: 'Listen to the latest episode (S1)' },
|
|
65
|
+
])('appends season info for $seasonMode', async ({ seasonMode, expected }) => {
|
|
66
|
+
const wrapper = await mountPage(seasonMode);
|
|
67
|
+
await triggerFetch(wrapper, seasonMode);
|
|
68
|
+
expect(wrapper.text()).toContain(expected);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('podcastsFetched season filtering', () => {
|
|
73
|
+
// seasonCount is 2 in makeEmission
|
|
74
|
+
it.each([undefined, 2])('shows lastPodcast when season is %s', async (season) => {
|
|
75
|
+
const wrapper = await mountPage(SeasonMode.SEASON_WITH_PODCAST_NUMBERING);
|
|
76
|
+
await triggerFetch(wrapper, SeasonMode.SEASON_WITH_PODCAST_NUMBERING, season);
|
|
77
|
+
expect(wrapper.text()).toContain('Listen to the latest episode');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('ignores podcasts from earlier seasons', async () => {
|
|
81
|
+
const wrapper = await mountPage(SeasonMode.SEASON_WITH_PODCAST_NUMBERING);
|
|
82
|
+
await triggerFetch(wrapper, SeasonMode.SEASON_WITH_PODCAST_NUMBERING, 1);
|
|
83
|
+
expect(wrapper.text()).not.toContain('Listen to the latest episode');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|