@saooti/octopus-sdk 41.9.0 → 41.9.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/CHANGELOG.md +23 -0
- package/index.ts +2 -1
- package/package.json +1 -1
- package/src/api/transcriptionApi.ts +67 -1
- package/src/components/composable/podcasts/usePodcastView.ts +2 -3
- package/src/components/composable/useDayjs.ts +24 -0
- package/src/components/composable/useRights.ts +11 -1
- package/src/components/composable/useTranslation.ts +5 -4
- package/src/components/display/podcasts/PodcastItemInfo.vue +3 -1
- package/src/components/display/podcasts/PodcastModuleBox.vue +13 -2
- package/src/components/display/podcasts/PodcastRawTranscript.vue +16 -5
- package/src/components/form/ClassicInputText.vue +11 -1
- package/src/components/misc/ClassicAlert.vue +26 -14
- package/src/components/pages/EmissionPage.vue +6 -3
- package/src/i18n.ts +0 -7
- package/src/stores/ParamSdkStore.ts +1 -1
- package/tests/components/composable/useRights.spec.ts +44 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 41.9.1 (07/04/2026)
|
|
4
|
+
|
|
5
|
+
**Features**
|
|
6
|
+
|
|
7
|
+
- **12535** - Améliorations pour la traduction
|
|
8
|
+
- La régénration de la transcription invalide les données de
|
|
9
|
+
`PodcastRawTranscript`
|
|
10
|
+
- **14406** - Ajout de `useDayjs` qui permet d'avoir des dates réactives
|
|
11
|
+
- Mise à jour des droits relatifs aux transcriptions/traductions
|
|
12
|
+
- Ajout de nouveaux endpoints à `transcriptionApi`
|
|
13
|
+
- Ajout nouveaux exports:
|
|
14
|
+
- composable `useTranslation`
|
|
15
|
+
- types `TranslationData`, `ModifyPodcastConfig`
|
|
16
|
+
- énumération `TranslationState`
|
|
17
|
+
- Améliorations composants :
|
|
18
|
+
- `ClassicInputText` en mode `textarea` scrolle en haut du contenu par défaut
|
|
19
|
+
- `ClassicAlert` a maintenant un props `text` qui enlève le font et la bordure
|
|
20
|
+
|
|
21
|
+
**Fixes**
|
|
22
|
+
|
|
23
|
+
- **14330** - La page d'émission propose uniquement le dernier épisode
|
|
24
|
+
**valide** à la lecture
|
|
25
|
+
|
|
3
26
|
## 41.9.0 (31/03/2026)
|
|
4
27
|
|
|
5
28
|
**Features**
|
package/index.ts
CHANGED
|
@@ -135,6 +135,7 @@ 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
137
|
export { useSeasonsManagement } from "./src/components/composable/useSeasonsManagement.ts";
|
|
138
|
+
export { useTranslation } from "./src/components/composable/useTranslation.ts";
|
|
138
139
|
|
|
139
140
|
//helper
|
|
140
141
|
import domHelper from "./src/helper/domHelper.ts";
|
|
@@ -161,7 +162,7 @@ import classicApi from "./src/api/classicApi.ts";
|
|
|
161
162
|
|
|
162
163
|
// API
|
|
163
164
|
export { emissionApi } from "./src/api/emissionApi.ts";
|
|
164
|
-
export { transcriptionApi } from "./src/api/transcriptionApi.ts";
|
|
165
|
+
export { transcriptionApi, type TranslationData, TranslationState } from "./src/api/transcriptionApi.ts";
|
|
165
166
|
export * from "./src/api/groupsApi.ts";
|
|
166
167
|
export { organisationApi } from "./src/api/organisationApi.ts";
|
|
167
168
|
export { playlistApi } from "./src/api/playlistApi.ts";
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ModifyPodcastConfig } from "../stores/class/transcript/transcriptParams";
|
|
1
2
|
import { ModuleApi } from "./apiConnection";
|
|
2
3
|
import classicApi from "./classicApi";
|
|
3
4
|
|
|
@@ -100,8 +101,73 @@ async function getRawTranscription(podcastId: number): Promise<string> {
|
|
|
100
101
|
});
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Generate the transcription on the given podcast
|
|
106
|
+
* @param podcastId ID of the podcast on which to do the transcription
|
|
107
|
+
* @param langauge Target language. Should be the native language of the podcast
|
|
108
|
+
* @param params Transcription parameters
|
|
109
|
+
*/
|
|
110
|
+
async function generateTranscription(podcastId: number, language: string, params: ModifyPodcastConfig): Promise<void> {
|
|
111
|
+
await classicApi.putData({
|
|
112
|
+
api: ModuleApi.SPEECHTOTEXT,
|
|
113
|
+
path: `convert/${language}/${podcastId}`,
|
|
114
|
+
dataToSend: params
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Regenerate the transcription on the given podcast
|
|
120
|
+
* Currently does the same as `generateTranscription`, but should be used when
|
|
121
|
+
* relevant in case of future evolutions.
|
|
122
|
+
* @param podcastId ID of the podcast on which to do the transcription
|
|
123
|
+
* @param langauge Target language. Should be the native language of the podcast
|
|
124
|
+
* @param params Transcription parameters
|
|
125
|
+
*/
|
|
126
|
+
async function regenerateTranscription(podcastId: number, language: string, params: ModifyPodcastConfig): Promise<void> {
|
|
127
|
+
await classicApi.putData({
|
|
128
|
+
api: ModuleApi.SPEECHTOTEXT,
|
|
129
|
+
path: `regenerate/${language}/${podcastId}`,
|
|
130
|
+
dataToSend: params
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Change the visibility of the podcast
|
|
136
|
+
* @param podcastId ID of the podcast for which to change the visibility
|
|
137
|
+
* @param visibility New visibility state
|
|
138
|
+
*/
|
|
139
|
+
async function changeTranscriptionVisibility(podcastId: number, visibility: boolean): Promise<void> {
|
|
140
|
+
await classicApi.putData({
|
|
141
|
+
api: 11,
|
|
142
|
+
path: "visibility/" + podcastId,
|
|
143
|
+
parameters: {
|
|
144
|
+
visibility
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Update the transcription of a podcast
|
|
151
|
+
* @param podcastId ID of the podcast
|
|
152
|
+
* @param transcript The new transcript
|
|
153
|
+
*/
|
|
154
|
+
async function updateTranscription(podcastId: number, transcript: string): Promise<void> {
|
|
155
|
+
await classicApi.postData({
|
|
156
|
+
api: ModuleApi.SPEECHTOTEXT,
|
|
157
|
+
path: "update/" + podcastId,
|
|
158
|
+
dataToSend: {
|
|
159
|
+
file: new File([transcript], "file")
|
|
160
|
+
},
|
|
161
|
+
contentType: "formData"
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
103
165
|
export const transcriptionApi = {
|
|
166
|
+
changeTranscriptionVisibility,
|
|
104
167
|
getTranslations,
|
|
105
168
|
getTranslation,
|
|
106
|
-
getRawTranscription
|
|
169
|
+
getRawTranscription,
|
|
170
|
+
generateTranscription,
|
|
171
|
+
regenerateTranscription,
|
|
172
|
+
updateTranscription
|
|
107
173
|
};
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { Conference } from '@/stores/class/conference/conference';
|
|
2
2
|
import { Podcast } from '@/stores/class/general/podcast';
|
|
3
|
-
import dayjs from 'dayjs';
|
|
4
|
-
import duration from "dayjs/plugin/duration";
|
|
5
|
-
dayjs.extend(duration);
|
|
6
3
|
// @ts-expect-error Bibliothèque non typée
|
|
7
4
|
import humanizeDuration from "humanize-duration";
|
|
8
5
|
import {computed, Ref} from 'vue';
|
|
9
6
|
import { useI18n } from 'vue-i18n';
|
|
10
7
|
import {useOrgaComputed} from "../useOrgaComputed"
|
|
11
8
|
import { state } from '../../../stores/ParamSdkStore';
|
|
9
|
+
import { useDayjs } from '../useDayjs';
|
|
12
10
|
|
|
13
11
|
export const usePodcastView = (podcast: Ref<Podcast|undefined>, podcastConference: Ref<Conference|undefined>)=>{
|
|
14
12
|
|
|
15
13
|
const {locale} = useI18n();
|
|
14
|
+
const { dayjs } = useDayjs();
|
|
16
15
|
|
|
17
16
|
const {isEditRights, isPodcastmaker} = useOrgaComputed();
|
|
18
17
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import dayjs, { Dayjs } from "dayjs";
|
|
2
|
+
import { useI18n } from "vue-i18n"
|
|
3
|
+
|
|
4
|
+
import "dayjs/locale/de";
|
|
5
|
+
import "dayjs/locale/es";
|
|
6
|
+
import "dayjs/locale/fr";
|
|
7
|
+
import "dayjs/locale/it";
|
|
8
|
+
import "dayjs/locale/sl";
|
|
9
|
+
|
|
10
|
+
import duration from "dayjs/plugin/duration";
|
|
11
|
+
dayjs.extend(duration);
|
|
12
|
+
|
|
13
|
+
export const useDayjs = () => {
|
|
14
|
+
const { locale } = useI18n();
|
|
15
|
+
|
|
16
|
+
function composableDayjs(param?: string|number|Date|Dayjs): Dayjs {
|
|
17
|
+
return dayjs(param).locale(locale.value);
|
|
18
|
+
}
|
|
19
|
+
composableDayjs.duration = dayjs.duration;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
dayjs: composableDayjs
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -154,13 +154,21 @@ export const useRights = () => {
|
|
|
154
154
|
return true;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
if (roleContainsAny('RESTRICTED_PRODUCTION', '
|
|
157
|
+
if (roleContainsAny('RESTRICTED_PRODUCTION', 'PODCAST_CRUD')) {
|
|
158
158
|
return podcast.createdByUserId === authStore.authProfile?.userId;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
return false;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
function canEditTranscriptVisibility(podcast: Podcast): boolean {
|
|
165
|
+
return roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function canEditTranslation(podcast: Podcast): boolean {
|
|
169
|
+
return canEditTranscript(podcast);
|
|
170
|
+
}
|
|
171
|
+
|
|
164
172
|
function canSeeHistory(): boolean {
|
|
165
173
|
return roleContainsAny('ADMIN', 'ORGANISATION');
|
|
166
174
|
}
|
|
@@ -198,6 +206,8 @@ export const useRights = () => {
|
|
|
198
206
|
// Other
|
|
199
207
|
canEditCodeInsertPlayer,
|
|
200
208
|
canEditTranscript,
|
|
209
|
+
canEditTranslation,
|
|
210
|
+
canEditTranscriptVisibility,
|
|
201
211
|
canSeeHistory,
|
|
202
212
|
isRestrictedProduction
|
|
203
213
|
}
|
|
@@ -64,7 +64,7 @@ export const useTranslation = () => {
|
|
|
64
64
|
* @param emission *(optional)* If set, will also check in emission settings
|
|
65
65
|
* @returns Whether the language is available or not
|
|
66
66
|
*/
|
|
67
|
-
function
|
|
67
|
+
function getTranslationConfig(language: string, emission?: Emission): CreateTranslation {
|
|
68
68
|
const orgAttributes = authStore.authOrganisation.attributes;
|
|
69
69
|
const emissionTranslation = parseOrDefault(emission?.annotations['translation-config'] as string|undefined, true);
|
|
70
70
|
const orgTranslation = parseOrDefault(orgAttributes?.['translation-config']);
|
|
@@ -93,8 +93,8 @@ export const useTranslation = () => {
|
|
|
93
93
|
const podcast = await podcastApi.get(translationData.podcastId);
|
|
94
94
|
const emission = podcast.emission;
|
|
95
95
|
|
|
96
|
-
const baseLangConfig =
|
|
97
|
-
const defaultLangConfig =
|
|
96
|
+
const baseLangConfig = getTranslationConfig(baseLanguage, emission);
|
|
97
|
+
const defaultLangConfig = getTranslationConfig(DEFAULT_LANGUAGE, emission);
|
|
98
98
|
|
|
99
99
|
// 2. If the language of the browser is available, use it
|
|
100
100
|
if (baseLangConfig === CreateTranslation.ALWAYS) {
|
|
@@ -169,6 +169,7 @@ export const useTranslation = () => {
|
|
|
169
169
|
return {
|
|
170
170
|
convertSrtToPlainText,
|
|
171
171
|
getMostRelevantLanguage,
|
|
172
|
-
getMostRelevantTranslation
|
|
172
|
+
getMostRelevantTranslation,
|
|
173
|
+
getTranslationConfig
|
|
173
174
|
}
|
|
174
175
|
};
|
|
@@ -60,7 +60,6 @@
|
|
|
60
60
|
<script setup lang="ts">
|
|
61
61
|
import AnimatorsItem from "./AnimatorsItem.vue";
|
|
62
62
|
import {useOrgaComputed} from "../../composable/useOrgaComputed";
|
|
63
|
-
import dayjs from "dayjs";
|
|
64
63
|
import { computed, defineAsyncComponent } from "vue";
|
|
65
64
|
import { Podcast, PodcastType } from "../../../stores/class/general/podcast";
|
|
66
65
|
import { state } from "../../../stores/ParamSdkStore";
|
|
@@ -68,6 +67,7 @@ import { useI18n } from "vue-i18n";
|
|
|
68
67
|
import { useSeasonsManagement } from "../../composable/useSeasonsManagement";
|
|
69
68
|
import BullhornIcon from 'vue-material-design-icons/Bullhorn.vue';
|
|
70
69
|
import GiftIcon from 'vue-material-design-icons/Gift.vue';
|
|
70
|
+
import { useDayjs } from "../../composable/useDayjs";
|
|
71
71
|
const PodcastPlayBar = defineAsyncComponent(
|
|
72
72
|
() => import("./PodcastPlayBar.vue"),
|
|
73
73
|
);
|
|
@@ -81,6 +81,7 @@ const props = defineProps({
|
|
|
81
81
|
const { t } = useI18n();
|
|
82
82
|
const { isPodcastmaker } = useOrgaComputed();
|
|
83
83
|
const { formatSeason } = useSeasonsManagement();
|
|
84
|
+
const { dayjs } = useDayjs();
|
|
84
85
|
|
|
85
86
|
//Computed
|
|
86
87
|
const date = computed(() => {
|
|
@@ -90,6 +91,7 @@ const date = computed(() => {
|
|
|
90
91
|
}
|
|
91
92
|
return dayjs(props.podcast.pubDate).format(format);
|
|
92
93
|
});
|
|
94
|
+
|
|
93
95
|
const orgaNameDisplay = computed(() =>{
|
|
94
96
|
if (props.podcast.organisation.name.length > 30) {
|
|
95
97
|
return props.podcast.organisation.name.substring(0, 30) + "...";
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
v-else-if="editRight && isEditBox"
|
|
13
13
|
:podcast="podcast"
|
|
14
14
|
:display-studio-access="isDebriefing"
|
|
15
|
+
@update-transcription="resetTranscription"
|
|
15
16
|
@validate-podcast="emit('updatePodcast', $event)"
|
|
16
17
|
/>
|
|
17
18
|
<div class="mb-2 w-100">
|
|
@@ -158,7 +159,10 @@
|
|
|
158
159
|
:orga-id="podcast.organisation.id"
|
|
159
160
|
:rubrique-ids="podcastRubriques"
|
|
160
161
|
/>
|
|
161
|
-
<PodcastRawTranscript
|
|
162
|
+
<PodcastRawTranscript
|
|
163
|
+
ref="podcastRawTranscript"
|
|
164
|
+
:podcast-id="podcast.podcastId"
|
|
165
|
+
/>
|
|
162
166
|
<SubscribeButtons
|
|
163
167
|
v-if="isPodcastmaker"
|
|
164
168
|
class="mt-4"
|
|
@@ -185,7 +189,7 @@ import { useRouter } from "vue-router";
|
|
|
185
189
|
import { useSeasonsManagement } from "../../composable/useSeasonsManagement";
|
|
186
190
|
import { SeasonMode } from "../../../stores/class/general/emission";
|
|
187
191
|
|
|
188
|
-
import { defineAsyncComponent, toRefs, computed } from "vue";
|
|
192
|
+
import { defineAsyncComponent, toRefs, computed, useTemplateRef } from "vue";
|
|
189
193
|
const ErrorMessage = defineAsyncComponent(
|
|
190
194
|
() => import("../../misc/ErrorMessage.vue"),
|
|
191
195
|
);
|
|
@@ -243,6 +247,9 @@ const authStore = useAuthStore();
|
|
|
243
247
|
const router = useRouter();
|
|
244
248
|
const { areSeasonsEnabled } = useSeasonsManagement();
|
|
245
249
|
|
|
250
|
+
/** Reference to the PodcastRqwTranscript */
|
|
251
|
+
const podcastRawTranscript = useTemplateRef('podcastRawTranscript');
|
|
252
|
+
|
|
246
253
|
//Computed
|
|
247
254
|
const podcastRubriques = computed(() => {
|
|
248
255
|
let rubriques = props.podcast?.rubriqueIds ?? [];
|
|
@@ -342,6 +349,10 @@ function removeDeleted(): void {
|
|
|
342
349
|
}
|
|
343
350
|
}
|
|
344
351
|
|
|
352
|
+
function resetTranscription(): void {
|
|
353
|
+
podcastRawTranscript.value.reset();
|
|
354
|
+
}
|
|
355
|
+
|
|
345
356
|
const showTags = computed((): boolean => {
|
|
346
357
|
if (state.podcastPage.hideTags === true) {
|
|
347
358
|
return false;
|
|
@@ -137,12 +137,14 @@ function saveAccessibility(accessibility: {fontSize: number,background: string,c
|
|
|
137
137
|
isAccessibilityModal.value = false;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
async function fetchTranscripts(): Promise<void> {
|
|
140
|
+
async function fetchTranscripts(language?: string): Promise<void> {
|
|
141
141
|
try {
|
|
142
142
|
const translation = await transcriptionApi.getTranslations(props.podcastId);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
if (language === undefined) {
|
|
144
|
+
const { ready, available } = await getMostRelevantLanguage(translation);
|
|
145
|
+
// If available language is set, it is better than ready, so use it
|
|
146
|
+
language = available ?? ready;
|
|
147
|
+
}
|
|
146
148
|
const srt = await transcriptionApi.getTranslation(props.podcastId, language, true);
|
|
147
149
|
transcript.value = convertSrtToPlainText(srt);
|
|
148
150
|
|
|
@@ -165,7 +167,7 @@ async function fetchTranscripts(): Promise<void> {
|
|
|
165
167
|
async function changeLanguage(language: string): Promise<void> {
|
|
166
168
|
loadingDone.value = false;
|
|
167
169
|
currentLanguage.value = language;
|
|
168
|
-
transcript.value =
|
|
170
|
+
transcript.value = undefined;
|
|
169
171
|
try {
|
|
170
172
|
const srt = await transcriptionApi.getTranslation(props.podcastId, language);
|
|
171
173
|
transcript.value = convertSrtToPlainText(srt);
|
|
@@ -174,6 +176,15 @@ async function changeLanguage(language: string): Promise<void> {
|
|
|
174
176
|
}
|
|
175
177
|
loadingDone.value = true;
|
|
176
178
|
}
|
|
179
|
+
|
|
180
|
+
/** Force reloading */
|
|
181
|
+
function reset(): void {
|
|
182
|
+
if (isOpen.value || loadingDone.value) {
|
|
183
|
+
fetchTranscripts(currentLanguage.value);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
defineExpose({ reset });
|
|
177
188
|
</script>
|
|
178
189
|
|
|
179
190
|
<style scoped lang="scss">
|
|
@@ -259,7 +259,17 @@ onMounted(()=>{
|
|
|
259
259
|
if (props.errorVariable !== isError.value) {
|
|
260
260
|
emit("update:errorVariable", isError.value);
|
|
261
261
|
}
|
|
262
|
-
|
|
262
|
+
|
|
263
|
+
if (props.isTextarea) {
|
|
264
|
+
// Delay a scroll back to the top of the text area
|
|
265
|
+
setTimeout(() => {
|
|
266
|
+
const textArea = document.getElementById(computedInputId.value);
|
|
267
|
+
if (textArea) {
|
|
268
|
+
textArea.scrollTop = 0;
|
|
269
|
+
}
|
|
270
|
+
}, 100);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
263
273
|
|
|
264
274
|
//Methods
|
|
265
275
|
function addEmojiSelected(emoji: string) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
-->
|
|
9
9
|
<template>
|
|
10
10
|
<div
|
|
11
|
-
class="p-2 pe-4
|
|
11
|
+
class="p-2 pe-4 rounded d-flex alert"
|
|
12
12
|
:class="cardClass"
|
|
13
13
|
>
|
|
14
14
|
<!-- The icon -->
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
:is="iconComponent"
|
|
17
17
|
v-if="!noIcon"
|
|
18
18
|
class="icon"
|
|
19
|
-
:size="30"
|
|
19
|
+
:size="text ? 22 : 30"
|
|
20
20
|
/>
|
|
21
21
|
|
|
22
22
|
<!-- Main content -->
|
|
@@ -39,18 +39,26 @@ import CheckCircle from 'vue-material-design-icons/CheckCircle.vue';
|
|
|
39
39
|
import CloseCircle from 'vue-material-design-icons/CloseCircle.vue';
|
|
40
40
|
import Information from 'vue-material-design-icons/Information.vue';
|
|
41
41
|
|
|
42
|
-
const { type } = defineProps<{
|
|
42
|
+
const { type, text } = defineProps<{
|
|
43
43
|
/** Disables the icon when true */
|
|
44
44
|
noIcon?: boolean;
|
|
45
45
|
/** An optional title for the alert */
|
|
46
46
|
title?: string;
|
|
47
47
|
/** The type of message */
|
|
48
48
|
type: 'info'|'success'|'warning'|'error';
|
|
49
|
+
/** Use a simpler display */
|
|
50
|
+
text?: boolean;
|
|
49
51
|
}>();
|
|
50
52
|
|
|
51
53
|
/** The class applied to the alert */
|
|
52
|
-
const cardClass = computed(() => {
|
|
53
|
-
|
|
54
|
+
const cardClass = computed((): Array<string> => {
|
|
55
|
+
const classes = ['alert-' + type];
|
|
56
|
+
if (text === true) {
|
|
57
|
+
classes.push('text-alert');
|
|
58
|
+
} else {
|
|
59
|
+
classes.push('my-2');
|
|
60
|
+
}
|
|
61
|
+
return classes;
|
|
54
62
|
});
|
|
55
63
|
|
|
56
64
|
const iconComponent = computed(() => {
|
|
@@ -70,9 +78,10 @@ const iconComponent = computed(() => {
|
|
|
70
78
|
|
|
71
79
|
<style lang="scss" scoped>
|
|
72
80
|
.alert {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
&:not(.text-alert) {
|
|
82
|
+
border: 1px solid;
|
|
83
|
+
border-left: 8px solid;
|
|
84
|
+
}
|
|
76
85
|
|
|
77
86
|
.icon {
|
|
78
87
|
align-items: start !important;
|
|
@@ -98,14 +107,17 @@ const iconComponent = computed(() => {
|
|
|
98
107
|
'error': var(--octopus-danger),
|
|
99
108
|
);
|
|
100
109
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
@each $type, $color in $types {
|
|
111
|
+
&-#{$type} {
|
|
112
|
+
&:not(.text-alert) {
|
|
104
113
|
background-color: hsl(from #{$color} h s 95) !important;
|
|
105
114
|
border-color: #{$color} !important;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
115
|
+
}
|
|
116
|
+
&.text-alert {
|
|
117
|
+
color: #{$color} !important;
|
|
118
|
+
}
|
|
119
|
+
.icon {
|
|
120
|
+
color: #{$color};
|
|
109
121
|
}
|
|
110
122
|
}
|
|
111
123
|
}
|
|
@@ -269,8 +269,11 @@ async function getEmissionDetails(): Promise<void> {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
function isReadyAndVisibleAndValid(p: Podcast): boolean {
|
|
273
|
+
return "READY" === p.processingStatus && p.availability.visibility && p.valid === true;
|
|
274
|
+
}
|
|
275
|
+
|
|
272
276
|
function podcastsFetched(podcasts: Array<Podcast>, season: number|undefined) {
|
|
273
|
-
const isReadyAndVisible = (p: Podcast) => "READY" === p.processingStatus && p.availability.visibility;
|
|
274
277
|
|
|
275
278
|
if (areSeasonsEnabled(emission.value)) {
|
|
276
279
|
// Ignore results that are not from the last season
|
|
@@ -279,11 +282,11 @@ function podcastsFetched(podcasts: Array<Podcast>, season: number|undefined) {
|
|
|
279
282
|
return;
|
|
280
283
|
}
|
|
281
284
|
// If seasons are enabled, take last element
|
|
282
|
-
lastPodcast.value = podcasts.findLast(
|
|
285
|
+
lastPodcast.value = podcasts.findLast(isReadyAndVisibleAndValid);
|
|
283
286
|
} else {
|
|
284
287
|
// If seasons are disabled, we have a standard date desc sort, so we take
|
|
285
288
|
// first element
|
|
286
|
-
lastPodcast.value = podcasts.find(
|
|
289
|
+
lastPodcast.value = podcasts.find(isReadyAndVisibleAndValid);
|
|
287
290
|
}
|
|
288
291
|
}
|
|
289
292
|
|
package/src/i18n.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { nextTick } from "vue";
|
|
2
2
|
import { createI18n } from "vue-i18n";
|
|
3
|
-
import dayjs from "dayjs";
|
|
4
|
-
import "dayjs/locale/de";
|
|
5
|
-
import "dayjs/locale/es";
|
|
6
|
-
import "dayjs/locale/fr";
|
|
7
|
-
import "dayjs/locale/it";
|
|
8
|
-
import "dayjs/locale/sl";
|
|
9
3
|
|
|
10
4
|
export function setupI18n(options: { [key:string]: string|boolean }, isAuthenticated: boolean, isEducation: boolean) {
|
|
11
5
|
const i18n = createI18n(options);
|
|
@@ -15,7 +9,6 @@ export function setupI18n(options: { [key:string]: string|boolean }, isAuthentic
|
|
|
15
9
|
|
|
16
10
|
export function setI18nLanguage(i18n: any, locale: string) {
|
|
17
11
|
i18n.locale.value= locale;
|
|
18
|
-
dayjs.locale(locale);
|
|
19
12
|
const html = document.querySelector("html");
|
|
20
13
|
if (html) {
|
|
21
14
|
html.setAttribute("lang", locale);
|
|
@@ -63,7 +63,7 @@ export interface ParamStore {
|
|
|
63
63
|
/** If true, do not display subtitles on podcast pages */
|
|
64
64
|
hideSubtitle?: boolean;
|
|
65
65
|
/** Select whether to display description, summary, or both */
|
|
66
|
-
descriptionOrSummary
|
|
66
|
+
descriptionOrSummary?: 'description'|'summary'|'both';
|
|
67
67
|
};
|
|
68
68
|
emissionPage: {
|
|
69
69
|
ShareButtons?: boolean;
|
|
@@ -256,7 +256,7 @@ describe('useRights', () => {
|
|
|
256
256
|
});
|
|
257
257
|
});
|
|
258
258
|
|
|
259
|
-
['RESTRICTED_PRODUCTION', '
|
|
259
|
+
['RESTRICTED_PRODUCTION', 'PODCAST_CRUD'].forEach(role => {
|
|
260
260
|
it(`${role} can only edit transcript of own podcast`, async () => {
|
|
261
261
|
await setup([role]);
|
|
262
262
|
expect(useRights().canEditTranscript(ownPodcast)).toBe(true);
|
|
@@ -265,11 +265,53 @@ describe('useRights', () => {
|
|
|
265
265
|
});
|
|
266
266
|
|
|
267
267
|
it('denies unrelated roles', async () => {
|
|
268
|
-
await setup(['
|
|
268
|
+
await setup(['RESTRICTED_ANIMATION']);
|
|
269
269
|
expect(useRights().canEditTranscript(ownPodcast)).toBe(false);
|
|
270
270
|
});
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
+
describe('canEditTranslation', () => {
|
|
274
|
+
const ownPodcast = { podcastId: 1, createdByUserId: 'test-user-123' } as Podcast;
|
|
275
|
+
const otherPodcast = { podcastId: 2, createdByUserId: 'other-user' } as Podcast;
|
|
276
|
+
|
|
277
|
+
['ADMIN', 'ORGANISATION', 'PRODUCTION'].forEach(role => {
|
|
278
|
+
it(`allows ${role} to edit transcript of any podcast`, async () => {
|
|
279
|
+
await setup([role]);
|
|
280
|
+
expect(useRights().canEditTranslation(otherPodcast)).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
['RESTRICTED_PRODUCTION', 'PODCAST_CRUD'].forEach(role => {
|
|
285
|
+
it(`${role} can only edit transcript of own podcast`, async () => {
|
|
286
|
+
await setup([role]);
|
|
287
|
+
expect(useRights().canEditTranslation(ownPodcast)).toBe(true);
|
|
288
|
+
expect(useRights().canEditTranslation(otherPodcast)).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('denies unrelated roles', async () => {
|
|
293
|
+
await setup(['RESTRICTED_ANIMATION']);
|
|
294
|
+
expect(useRights().canEditTranslation(ownPodcast)).toBe(false);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('canEditTranscriptVisibility', () => {
|
|
299
|
+
const ownPodcast = { podcastId: 1, createdByUserId: 'test-user-123' } as Podcast;
|
|
300
|
+
const otherPodcast = { podcastId: 2, createdByUserId: 'other-user' } as Podcast;
|
|
301
|
+
|
|
302
|
+
['ADMIN', 'ORGANISATION', 'PRODUCTION'].forEach(role => {
|
|
303
|
+
it(`allows ${role} to edit transcript of any podcast`, async () => {
|
|
304
|
+
await setup([role]);
|
|
305
|
+
expect(useRights().canEditTranscriptVisibility(otherPodcast)).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('denies unrelated roles', async () => {
|
|
310
|
+
await setup(['PODCAST_CRUD']);
|
|
311
|
+
expect(useRights().canEditTranscriptVisibility(ownPodcast)).toBe(false);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
273
315
|
describe('Other permissions', () => {
|
|
274
316
|
describe('canEditCodeInsertPlayer', () => {
|
|
275
317
|
['ADMIN', 'ORGANISATION'].forEach(role => {
|