@windward/core 0.26.0 → 0.28.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 +66 -0
- package/components/Content/Blocks/Accordion.vue +9 -15
- package/components/Content/Blocks/BlockQuote.vue +29 -4
- package/components/Content/Blocks/ClickableIcons.vue +22 -9
- package/components/Content/Blocks/Email.vue +11 -4
- package/components/Content/Blocks/Feedback/FeedbackAnalytics.vue +179 -0
- package/components/Content/Blocks/Feedback.vue +115 -111
- package/components/Content/Blocks/FileDownload.vue +2 -2
- package/components/Content/Blocks/Image.vue +144 -0
- package/components/Content/Blocks/OpenResponse.vue +419 -5
- package/components/Content/Blocks/ScenarioChoice.vue +11 -2
- package/components/Content/Blocks/Tab.vue +16 -29
- package/components/Content/Blocks/UserUpload.vue +66 -38
- package/components/Content/Blocks/Video.vue +377 -28
- package/components/Settings/AccordionSettings.vue +3 -15
- package/components/Settings/BlockQuoteSettings.vue +6 -4
- package/components/Settings/ClickableIconsSettings.vue +24 -10
- package/components/Settings/EmailSettings.vue +3 -11
- package/components/Settings/FileDownloadSettings.vue +8 -2
- package/components/Settings/ImageSettings.vue +26 -0
- package/components/Settings/OpenResponseCollateSettings.vue +10 -0
- package/components/Settings/OpenResponseSettings.vue +67 -7
- package/components/Settings/ScenarioChoiceSettings.vue +11 -5
- package/components/Settings/TabSettings.vue +3 -18
- package/components/Settings/UserUploadSettings.vue +16 -8
- package/components/Settings/VideoSettings/SourcePicker.vue +55 -21
- package/components/Settings/VideoSettings.vue +18 -2
- package/components/utils/ContentViewer.vue +180 -1
- package/components/utils/glossary/GlossaryToolTip.vue +4 -23
- package/helpers/GlossaryHelper.ts +4 -7
- package/i18n/en-US/components/content/blocks/accordion.ts +3 -0
- package/i18n/en-US/components/content/blocks/block_quote.ts +3 -1
- package/i18n/en-US/components/content/blocks/feedback.ts +2 -0
- package/i18n/en-US/components/content/blocks/file_download.ts +2 -1
- package/i18n/en-US/components/content/blocks/index.ts +2 -0
- package/i18n/en-US/components/content/blocks/open_response.ts +19 -1
- package/i18n/en-US/components/content/blocks/open_response_collate.ts +1 -1
- package/i18n/en-US/components/content/blocks/scenario_choice.ts +2 -0
- package/i18n/en-US/components/content/blocks/user_upload.ts +2 -1
- package/i18n/en-US/components/settings/accordion.ts +2 -1
- package/i18n/en-US/components/settings/block_quote.ts +1 -1
- package/i18n/en-US/components/settings/clickable_icon.ts +5 -0
- package/i18n/en-US/components/settings/email.ts +2 -1
- package/i18n/en-US/components/settings/file_download.ts +2 -2
- package/i18n/en-US/components/settings/image.ts +1 -0
- package/i18n/en-US/components/settings/open_response.ts +8 -0
- package/i18n/en-US/components/settings/open_response_collate.ts +3 -0
- package/i18n/en-US/components/settings/scenario_choice.ts +3 -1
- package/i18n/en-US/components/settings/tab.ts +4 -3
- package/i18n/en-US/components/settings/user_upload.ts +1 -0
- package/i18n/en-US/components/settings/video.ts +3 -1
- package/i18n/en-US/shared/content_blocks.ts +1 -1
- package/i18n/es-ES/components/content/blocks/accordion.ts +3 -0
- package/i18n/es-ES/components/content/blocks/block_quote.ts +3 -1
- package/i18n/es-ES/components/content/blocks/feedback.ts +2 -0
- package/i18n/es-ES/components/content/blocks/file_download.ts +2 -1
- package/i18n/es-ES/components/content/blocks/index.ts +2 -0
- package/i18n/es-ES/components/content/blocks/open_response.ts +19 -2
- package/i18n/es-ES/components/content/blocks/open_response_collate.ts +1 -1
- package/i18n/es-ES/components/content/blocks/scenario_choice.ts +2 -0
- package/i18n/es-ES/components/content/blocks/user_upload.ts +2 -1
- package/i18n/es-ES/components/settings/accordion.ts +4 -2
- package/i18n/es-ES/components/settings/block_quote.ts +1 -1
- package/i18n/es-ES/components/settings/clickable_icon.ts +7 -0
- package/i18n/es-ES/components/settings/email.ts +2 -1
- package/i18n/es-ES/components/settings/image.ts +1 -0
- package/i18n/es-ES/components/settings/open_response.ts +8 -0
- package/i18n/es-ES/components/settings/open_response_collate.ts +3 -0
- package/i18n/es-ES/components/settings/scenario_choice.ts +3 -1
- package/i18n/es-ES/components/settings/tab.ts +3 -2
- package/i18n/es-ES/components/settings/user_upload.ts +1 -0
- package/i18n/es-ES/components/settings/video.ts +3 -1
- package/i18n/es-ES/shared/content_blocks.ts +1 -1
- package/i18n/sv-SE/components/content/blocks/accordion.ts +3 -0
- package/i18n/sv-SE/components/content/blocks/block_quote.ts +3 -1
- package/i18n/sv-SE/components/content/blocks/feedback.ts +2 -0
- package/i18n/sv-SE/components/content/blocks/file_download.ts +2 -1
- package/i18n/sv-SE/components/content/blocks/index.ts +2 -0
- package/i18n/sv-SE/components/content/blocks/open_response.ts +19 -2
- package/i18n/sv-SE/components/content/blocks/open_response_collate.ts +1 -1
- package/i18n/sv-SE/components/content/blocks/scenario_choice.ts +2 -0
- package/i18n/sv-SE/components/content/blocks/user_upload.ts +2 -1
- package/i18n/sv-SE/components/settings/accordion.ts +2 -1
- package/i18n/sv-SE/components/settings/block_quote.ts +1 -1
- package/i18n/sv-SE/components/settings/clickable_icon.ts +6 -0
- package/i18n/sv-SE/components/settings/email.ts +2 -1
- package/i18n/sv-SE/components/settings/image.ts +1 -0
- package/i18n/sv-SE/components/settings/open_response.ts +8 -0
- package/i18n/sv-SE/components/settings/open_response_collate.ts +3 -0
- package/i18n/sv-SE/components/settings/scenario_choice.ts +3 -1
- package/i18n/sv-SE/components/settings/tab.ts +5 -3
- package/i18n/sv-SE/components/settings/user_upload.ts +1 -0
- package/i18n/sv-SE/components/settings/video.ts +3 -1
- package/i18n/sv-SE/shared/content_blocks.ts +1 -1
- package/models/SurveyResultMetric.ts +8 -0
- package/package.json +2 -2
- package/plugin.js +8 -0
- package/test/Components/Content/Blocks/Feedback/FeedbackTemplates/FeedbackAnalytics.spec.js +23 -0
- package/test/Components/Content/Blocks/{FeedbackTemplates → Feedback/FeedbackTemplates}/FeedbackQuestionLikert.spec.js +1 -1
- package/test/Components/Content/Blocks/{FeedbackTemplates → Feedback/FeedbackTemplates}/FeedbackQuestionOpenResponse.spec.js +1 -1
- package/test/Components/Content/Blocks/{FeedbackTemplates → Feedback/FeedbackTemplates}/FeedbackQuestionTrueFalse.spec.js +1 -1
- package/test/Components/Settings/AccordionSettings.spec.js +0 -13
- package/test/Components/Settings/ClickableIconsSettings.spec.js +1 -12
- package/test/Components/Settings/EmailSettings.spec.js +0 -9
- package/test/Components/Settings/TabSettings.spec.js +0 -13
- package/test/helpers/GlossaryHelper.spec.js +8 -8
- package/components/Content/Blocks/{FeedbackTemplates → Feedback/FeedbackTemplates}/FeedbackQuestionLikert.vue +1 -1
- package/components/Content/Blocks/{FeedbackTemplates → Feedback/FeedbackTemplates}/FeedbackQuestionOpenResponse.vue +1 -1
- /package/components/Content/Blocks/{FeedbackTemplates → Feedback/FeedbackTemplates}/FeedbackQuestionTrueFalse.vue +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-container class="pa-0">
|
|
2
|
+
<v-container :class="['pa-0', blockDirectionClasses]" :dir="blockTextDirection">
|
|
3
3
|
<div>
|
|
4
4
|
<h2
|
|
5
5
|
v-if="
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
</p>
|
|
18
18
|
</v-col>
|
|
19
19
|
<v-col v-if="!blockExists" cols="12">
|
|
20
|
-
<v-alert
|
|
20
|
+
<v-alert type="warning">
|
|
21
21
|
<p>
|
|
22
22
|
{{
|
|
23
23
|
$t(
|
|
@@ -28,44 +28,47 @@
|
|
|
28
28
|
</v-alert>
|
|
29
29
|
</v-col>
|
|
30
30
|
</v-row>
|
|
31
|
-
<v-row>
|
|
31
|
+
<v-row v-if="render">
|
|
32
32
|
<v-col cols="12">
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
>
|
|
46
|
-
<FileDropZone
|
|
47
|
-
v-model="uploadFiles"
|
|
48
|
-
:accept="
|
|
49
|
-
block.metadata.config.uploadSettings.accept
|
|
50
|
-
"
|
|
51
|
-
:multiple="
|
|
52
|
-
block.metadata.config.uploadSettings
|
|
53
|
-
.multiple
|
|
54
|
-
"
|
|
55
|
-
></FileDropZone>
|
|
56
|
-
|
|
57
|
-
<v-container class="text-center">
|
|
58
|
-
<v-btn
|
|
59
|
-
:disabled="!canUpload || loading"
|
|
60
|
-
color="primary"
|
|
61
|
-
elevation="0"
|
|
62
|
-
class="text-center"
|
|
63
|
-
@click="handleUpload"
|
|
33
|
+
<div class="upload-container" :class="uploadContainerClass">
|
|
34
|
+
<DisplayUserFilesTable
|
|
35
|
+
v-model="userFileAsset"
|
|
36
|
+
:enrollment="enrollment"
|
|
37
|
+
></DisplayUserFilesTable>
|
|
38
|
+
<div v-if="showUpload">
|
|
39
|
+
<div v-if="blockExists">
|
|
40
|
+
<v-form
|
|
41
|
+
ref="form"
|
|
42
|
+
v-model="valid"
|
|
43
|
+
lazy-validation
|
|
44
|
+
class="pb-0"
|
|
64
45
|
>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
46
|
+
<FileDropZone
|
|
47
|
+
v-model="uploadFiles"
|
|
48
|
+
:accept="
|
|
49
|
+
block.metadata.config.uploadSettings
|
|
50
|
+
.accept
|
|
51
|
+
"
|
|
52
|
+
:multiple="
|
|
53
|
+
block.metadata.config.uploadSettings
|
|
54
|
+
.multiple
|
|
55
|
+
"
|
|
56
|
+
></FileDropZone>
|
|
57
|
+
|
|
58
|
+
<v-container class="text-center">
|
|
59
|
+
<v-btn
|
|
60
|
+
:disabled="!canUpload || loading"
|
|
61
|
+
color="primary"
|
|
62
|
+
elevation="0"
|
|
63
|
+
class="text-center"
|
|
64
|
+
@click="handleUpload"
|
|
65
|
+
>
|
|
66
|
+
{{ $t('shared.forms.upload') }}
|
|
67
|
+
</v-btn>
|
|
68
|
+
</v-container>
|
|
69
|
+
</v-form>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
69
72
|
</div>
|
|
70
73
|
</v-col>
|
|
71
74
|
</v-row>
|
|
@@ -109,6 +112,11 @@ export default {
|
|
|
109
112
|
...mapGetters({
|
|
110
113
|
enrollment: 'enrollment/get',
|
|
111
114
|
}),
|
|
115
|
+
uploadContainerClass() {
|
|
116
|
+
return this.$vuetify.theme.dark
|
|
117
|
+
? 'upload-container--dark'
|
|
118
|
+
: 'upload-container--light'
|
|
119
|
+
},
|
|
112
120
|
blockExists() {
|
|
113
121
|
return Uuid.test(this.block.id)
|
|
114
122
|
},
|
|
@@ -255,3 +263,23 @@ export default {
|
|
|
255
263
|
},
|
|
256
264
|
}
|
|
257
265
|
</script>
|
|
266
|
+
|
|
267
|
+
<style scoped>
|
|
268
|
+
.upload-container {
|
|
269
|
+
padding: 16px;
|
|
270
|
+
border-radius: 4px;
|
|
271
|
+
border: 1px solid;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.upload-container--light {
|
|
275
|
+
background-color: #ffffff;
|
|
276
|
+
border-color: #e0e0e0;
|
|
277
|
+
color: #000000;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.upload-container--dark {
|
|
281
|
+
background-color: #424242;
|
|
282
|
+
border-color: #616161;
|
|
283
|
+
color: #ffffff;
|
|
284
|
+
}
|
|
285
|
+
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div :class="blockDirectionClasses" :dir="blockTextDirection">
|
|
3
3
|
<h2
|
|
4
4
|
v-if="
|
|
5
5
|
block.metadata.config.title &&
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{{ block.metadata.config.instructions }}
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
|
-
<div class="video-wrapper">
|
|
16
|
+
<div v-if="hasSource" class="video-wrapper" dir="ltr">
|
|
17
17
|
<VuetifyPlayer
|
|
18
18
|
class="no-border-player"
|
|
19
19
|
:language="$i18n && $i18n.locale ? $i18n.locale : 'en-US'"
|
|
@@ -74,6 +74,9 @@
|
|
|
74
74
|
</template>
|
|
75
75
|
</VuetifyPlayer>
|
|
76
76
|
</div>
|
|
77
|
+
<v-alert v-else type="warning">
|
|
78
|
+
{{ $t('windward.core.components.settings.video.no_video_alert') }}
|
|
79
|
+
</v-alert>
|
|
77
80
|
<!-- display first note in the playlist for now -->
|
|
78
81
|
<v-alert
|
|
79
82
|
v-if="notes.length > 0"
|
|
@@ -143,6 +146,124 @@ export default {
|
|
|
143
146
|
}
|
|
144
147
|
},
|
|
145
148
|
computed: {
|
|
149
|
+
courseSourceLocale() {
|
|
150
|
+
// Get the source language from translation metadata
|
|
151
|
+
// This is the original language the course was translated FROM
|
|
152
|
+
// For translated courses: metadata.translation.source_language (e.g., "EN")
|
|
153
|
+
// For non-translated courses: falls back to locale code
|
|
154
|
+
|
|
155
|
+
// First check translation metadata for source_language (e.g., "EN", "ES")
|
|
156
|
+
const translationSourceLang = _.get(this.block, 'course.metadata.translation.source_language') ||
|
|
157
|
+
this.$store?.getters?.['course/get']?.metadata?.translation?.source_language
|
|
158
|
+
if (translationSourceLang) {
|
|
159
|
+
return translationSourceLang.toLowerCase()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Try source_locale_id lookup
|
|
163
|
+
const translationSourceLocale = _.get(this.block, 'course.metadata.translation.source_locale_id')
|
|
164
|
+
if (translationSourceLocale) {
|
|
165
|
+
const sourceLocale = this.$store?.getters?.['locales/getById']?.(translationSourceLocale)
|
|
166
|
+
if (sourceLocale?.code) {
|
|
167
|
+
return sourceLocale.code.toLowerCase()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
_.get(this.block, 'course.locale.code')?.toLowerCase() ||
|
|
173
|
+
this.$store?.getters?.['course/sourceLocale']?.code?.toLowerCase() ||
|
|
174
|
+
'en-us'
|
|
175
|
+
)
|
|
176
|
+
},
|
|
177
|
+
courseTargetLocale() {
|
|
178
|
+
// Get the target language from translation metadata
|
|
179
|
+
// This is the language the course was translated INTO
|
|
180
|
+
// For translated courses: metadata.translation.target_language (e.g., "PT-BR", "ES")
|
|
181
|
+
|
|
182
|
+
const translationTargetLang = _.get(this.block, 'course.metadata.translation.target_language') ||
|
|
183
|
+
this.$store?.getters?.['course/get']?.metadata?.translation?.target_language
|
|
184
|
+
if (translationTargetLang) {
|
|
185
|
+
return translationTargetLang.toLowerCase()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Try target_locale_id lookup
|
|
189
|
+
const translationTargetLocale = _.get(this.block, 'course.metadata.translation.target_locale_id')
|
|
190
|
+
if (translationTargetLocale) {
|
|
191
|
+
const targetLocale = this.$store?.getters?.['locales/getById']?.(translationTargetLocale)
|
|
192
|
+
if (targetLocale?.code) {
|
|
193
|
+
return targetLocale.code.toLowerCase()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// No target language - course is not translated
|
|
198
|
+
return null
|
|
199
|
+
},
|
|
200
|
+
courseCurrentLocale() {
|
|
201
|
+
// Get the current course's locale code (the language this course is in)
|
|
202
|
+
// For a translated course, this is the target language
|
|
203
|
+
// Priority: translation target_language > block.course.locale > store getters > fallback
|
|
204
|
+
|
|
205
|
+
// For translated courses, use target_language directly
|
|
206
|
+
if (this.courseTargetLocale) {
|
|
207
|
+
return this.courseTargetLocale
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const localeCode =
|
|
211
|
+
_.get(this.block, 'course.locale.code')?.toLowerCase() ||
|
|
212
|
+
_.get(this.block, 'course.locale.name')?.toLowerCase() ||
|
|
213
|
+
this.$store?.getters?.['course/localeCode']?.toLowerCase() ||
|
|
214
|
+
this.$store?.getters?.['course/locale']?.code?.toLowerCase() ||
|
|
215
|
+
this.$store?.getters?.['course/locale']?.name?.toLowerCase() ||
|
|
216
|
+
this.$store?.state?.course?.course?.locale?.code?.toLowerCase() ||
|
|
217
|
+
this.$store?.state?.course?.course?.locale?.name?.toLowerCase()
|
|
218
|
+
|
|
219
|
+
return localeCode || this.courseSourceLocale
|
|
220
|
+
},
|
|
221
|
+
isTranslatedCourse() {
|
|
222
|
+
// Check if this course is a translated course (has translation metadata)
|
|
223
|
+
return !!(
|
|
224
|
+
_.get(this.block, 'course.metadata.translation') ||
|
|
225
|
+
this.$store?.getters?.['course/get']?.metadata?.translation
|
|
226
|
+
)
|
|
227
|
+
},
|
|
228
|
+
courseTargetLocaleId() {
|
|
229
|
+
// Get the target_locale_id from translation metadata (UUID)
|
|
230
|
+
// This is used to match VTT files which are linked by locale_id
|
|
231
|
+
return _.get(this.block, 'course.metadata.translation.target_locale_id') ||
|
|
232
|
+
this.$store?.getters?.['course/get']?.metadata?.translation?.target_locale_id ||
|
|
233
|
+
_.get(this.block, 'course.locale_id') ||
|
|
234
|
+
this.$store?.getters?.['course/get']?.locale_id ||
|
|
235
|
+
null
|
|
236
|
+
},
|
|
237
|
+
allowedCaptionLocales() {
|
|
238
|
+
// Only show captions for the source language and the current course's language
|
|
239
|
+
// This prevents showing all translated captions (FR, ES, DE, etc.) when viewing a specific course
|
|
240
|
+
const locales = new Set()
|
|
241
|
+
|
|
242
|
+
// Helper to add both full locale and base language code
|
|
243
|
+
const addLocale = (locale) => {
|
|
244
|
+
if (!locale) return
|
|
245
|
+
locales.add(locale)
|
|
246
|
+
locales.add(locale.toLowerCase())
|
|
247
|
+
// Also add base language code (e.g., "en" from "en-US", "fr" from "fr-FR")
|
|
248
|
+
const base = locale.split('-')[0]?.toLowerCase()
|
|
249
|
+
if (base) {
|
|
250
|
+
locales.add(base)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Always include the source language (original English captions)
|
|
255
|
+
addLocale(this.courseSourceLocale)
|
|
256
|
+
|
|
257
|
+
// Include the current course's language
|
|
258
|
+
addLocale(this.courseCurrentLocale)
|
|
259
|
+
|
|
260
|
+
// Also include tracks with no locale (legacy/default tracks)
|
|
261
|
+
locales.add(null)
|
|
262
|
+
locales.add(undefined)
|
|
263
|
+
locales.add('')
|
|
264
|
+
|
|
265
|
+
return locales
|
|
266
|
+
},
|
|
146
267
|
hasSource() {
|
|
147
268
|
// __isDirty is used to communicate from the settings panel that a source / caption
|
|
148
269
|
// has changed and to reload the video player
|
|
@@ -172,12 +293,38 @@ export default {
|
|
|
172
293
|
let file = this.resolveAsset(
|
|
173
294
|
playlist[index].sources[sourceIndex]
|
|
174
295
|
)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
296
|
+
|
|
297
|
+
// Try to get translated notes from VTT file (for translated courses)
|
|
298
|
+
let notes = null
|
|
299
|
+
if (this.isTranslatedCourse) {
|
|
300
|
+
const linkedCaptions = this.getAllLinkedCaptions(file)
|
|
301
|
+
// Find VTT matching by locale_id (UUID) or locale.code (language code)
|
|
302
|
+
const currentVtt = _.find(linkedCaptions, vtt => {
|
|
303
|
+
// First try matching by locale_id (most reliable)
|
|
304
|
+
if (this.courseTargetLocaleId && vtt.locale_id === this.courseTargetLocaleId) {
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
307
|
+
// Also try matching by locale.code
|
|
308
|
+
const vttLocale = _.get(vtt, 'locale.code', '').toLowerCase()
|
|
309
|
+
return vttLocale && this.isAllowedCaptionLanguage(vttLocale) &&
|
|
310
|
+
vttLocale === this.courseCurrentLocale
|
|
311
|
+
})
|
|
312
|
+
// Notes can be in different locations:
|
|
313
|
+
// - vtt.metadata.notes (FileAssetMap metadata)
|
|
314
|
+
// - vtt.notes (FileAssetMapResource exposes this)
|
|
315
|
+
// - vtt.asset.metadata.notes (FileAsset metadata - where translation stores them)
|
|
316
|
+
notes = _.get(currentVtt, 'metadata.notes') ||
|
|
317
|
+
_.get(currentVtt, 'notes') ||
|
|
318
|
+
_.get(currentVtt, 'asset.metadata.notes')
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Fallback to video asset notes
|
|
322
|
+
if (!notes || !this.hasWords(notes)) {
|
|
323
|
+
notes = _.get(file, 'asset.metadata.notes')
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (notes && this.hasWords(notes)) {
|
|
327
|
+
result.push(notes)
|
|
181
328
|
}
|
|
182
329
|
}
|
|
183
330
|
}
|
|
@@ -189,36 +336,123 @@ export default {
|
|
|
189
336
|
linkedPlaylist() {
|
|
190
337
|
const playlist = _.cloneDeep(this.block.metadata.config.playlist)
|
|
191
338
|
for (const index in playlist) {
|
|
339
|
+
// Build comprehensive track list from multiple sources
|
|
340
|
+
const allTracks = []
|
|
341
|
+
const addedSrcs = new Set() // Track added URLs to avoid duplicates
|
|
342
|
+
const addedLangs = new Set() // Track added languages to avoid duplicate srclang keys
|
|
343
|
+
|
|
344
|
+
// 1. FIRST: Resolve video files and get linked captions BEFORE transforming sources
|
|
345
|
+
const resolvedVideoFiles = []
|
|
192
346
|
for (const sourceIndex in playlist[index].sources) {
|
|
193
|
-
let
|
|
347
|
+
let videoFile = this.resolveAsset(
|
|
194
348
|
playlist[index].sources[sourceIndex]
|
|
195
349
|
)
|
|
350
|
+
resolvedVideoFiles.push(videoFile)
|
|
196
351
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
type: _.get(file, 'asset.metadata.mime', ''),
|
|
200
|
-
}
|
|
352
|
+
// Get ALL linked captions from this video file (adds to block.assets)
|
|
353
|
+
const linkedCaptions = this.getAllLinkedCaptions(videoFile)
|
|
201
354
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
355
|
+
for (const vtt of linkedCaptions) {
|
|
356
|
+
// Get the src - may be directly available or need to extract from asset
|
|
357
|
+
const src = _.get(vtt, 'asset.public_url', '')
|
|
358
|
+
|
|
359
|
+
// Skip if no src or already added
|
|
360
|
+
if (!src || addedSrcs.has(src)) {
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const srclang = _.get(vtt, 'locale.code') || this.courseSourceLocale
|
|
365
|
+
|
|
366
|
+
// Skip if this language was already added (avoid duplicate keys)
|
|
367
|
+
if (addedLangs.has(srclang)) {
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// FILTER: Only include tracks that match the course's language(s)
|
|
372
|
+
// - Original courses: show only the course's own language
|
|
373
|
+
// - Translated courses: show source language + target language
|
|
374
|
+
if (!this.isAllowedCaptionLanguage(srclang)) {
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const label = this.getLabelForLanguage(srclang) || _.get(vtt, 'locale.name') || srclang
|
|
379
|
+
|
|
380
|
+
allTracks.push({
|
|
381
|
+
src: src,
|
|
382
|
+
kind: 'captions',
|
|
383
|
+
srclang: srclang,
|
|
384
|
+
label: label,
|
|
385
|
+
default: false,
|
|
386
|
+
})
|
|
387
|
+
addedSrcs.add(src)
|
|
388
|
+
addedLangs.add(srclang)
|
|
207
389
|
}
|
|
208
390
|
}
|
|
209
391
|
|
|
392
|
+
// 2. Process manually-added tracks from playlist[x].tracks[]
|
|
210
393
|
for (const trackIndex in playlist[index].tracks) {
|
|
211
394
|
let file = this.resolveAsset(
|
|
212
395
|
playlist[index].tracks[trackIndex]
|
|
213
396
|
)
|
|
214
397
|
|
|
215
|
-
playlist[index].tracks[trackIndex]
|
|
216
|
-
|
|
398
|
+
const trackObj = playlist[index].tracks[trackIndex]
|
|
399
|
+
const src = _.get(file, 'asset.public_url', '')
|
|
400
|
+
|
|
401
|
+
// Skip if no src or already added
|
|
402
|
+
if (!src || addedSrcs.has(src)) {
|
|
403
|
+
continue
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const srclang = trackObj.srclang || this.courseSourceLocale
|
|
407
|
+
|
|
408
|
+
// Skip if this language was already added (avoid duplicate keys)
|
|
409
|
+
if (addedLangs.has(srclang)) {
|
|
410
|
+
continue
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// FILTER: Only include tracks that match source or target language
|
|
414
|
+
// For translated courses, only show source language and target language tracks
|
|
415
|
+
// For non-translated courses, this filter is skipped and all tracks are shown
|
|
416
|
+
if (this.isTranslatedCourse && !this.isAllowedCaptionLanguage(srclang)) {
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const label = trackObj.label || this.getLabelForLanguage(srclang)
|
|
421
|
+
|
|
422
|
+
allTracks.push({
|
|
423
|
+
src: src,
|
|
217
424
|
kind: 'captions',
|
|
218
|
-
srclang:
|
|
219
|
-
|
|
425
|
+
srclang: srclang,
|
|
426
|
+
label: label,
|
|
427
|
+
default: false,
|
|
428
|
+
})
|
|
429
|
+
addedSrcs.add(src)
|
|
430
|
+
addedLangs.add(srclang)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 3. NOW transform video sources to player format
|
|
434
|
+
for (const sourceIndex in playlist[index].sources) {
|
|
435
|
+
const file = resolvedVideoFiles[sourceIndex]
|
|
436
|
+
playlist[index].sources[sourceIndex] = {
|
|
437
|
+
src: _.get(file, 'asset.public_url', ''),
|
|
438
|
+
type: _.get(file, 'asset.metadata.mime', ''),
|
|
220
439
|
}
|
|
221
440
|
}
|
|
441
|
+
|
|
442
|
+
// 4. Sort tracks: course source language first
|
|
443
|
+
allTracks.sort((a, b) => {
|
|
444
|
+
if (a.srclang === this.courseSourceLocale) return -1
|
|
445
|
+
if (b.srclang === this.courseSourceLocale) return 1
|
|
446
|
+
return 0
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
// 5. Set default on first track (should be source language)
|
|
450
|
+
if (allTracks.length > 0) {
|
|
451
|
+
allTracks[0].default = true
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 6. Replace tracks array with processed tracks
|
|
455
|
+
playlist[index].tracks = allTracks
|
|
222
456
|
for (const adIndex in playlist[index].ads) {
|
|
223
457
|
for (const adSourceIndex in playlist[index].ads[adIndex]
|
|
224
458
|
.sources) {
|
|
@@ -351,6 +585,9 @@ export default {
|
|
|
351
585
|
},
|
|
352
586
|
},
|
|
353
587
|
},
|
|
588
|
+
mounted() {
|
|
589
|
+
// Video block mounted - locale filtering applied via isTranslatedCourse check
|
|
590
|
+
},
|
|
354
591
|
beforeMount() {
|
|
355
592
|
// Apply the default config
|
|
356
593
|
if (_.isEmpty(this.block.metadata.config)) {
|
|
@@ -397,15 +634,64 @@ export default {
|
|
|
397
634
|
async onBeforeSave() {
|
|
398
635
|
this.block.body = 'video'
|
|
399
636
|
},
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
637
|
+
/**
|
|
638
|
+
* Get ALL linked VTT caption files (supports multiple languages)
|
|
639
|
+
* @param {Object} file - The video file object with linked_assets
|
|
640
|
+
* @returns {Array} Array of VTT files with locale information
|
|
641
|
+
*/
|
|
642
|
+
getAllLinkedCaptions(file) {
|
|
643
|
+
// Get ALL VTT files from linked_assets (not just first one)
|
|
644
|
+
const linkedCaptions = _.filter(
|
|
403
645
|
_.get(file, 'asset.linked_assets', []),
|
|
404
|
-
|
|
405
|
-
return _.get(f, 'asset.metadata.extension', '') === 'vtt'
|
|
406
|
-
}
|
|
646
|
+
(f) => _.get(f, 'asset.metadata.extension', '') === 'vtt'
|
|
407
647
|
)
|
|
408
648
|
|
|
649
|
+
// Add locale information to each VTT AND add to block.assets
|
|
650
|
+
return linkedCaptions.map(vtt => {
|
|
651
|
+
// CRITICAL: Add to block.assets so resolveAsset can find it
|
|
652
|
+
const foundAsset = this.block.assets.find((a) => {
|
|
653
|
+
return a.file_asset_id === vtt.file_asset_id
|
|
654
|
+
})
|
|
655
|
+
if (!foundAsset) {
|
|
656
|
+
this.block.assets.push(_.cloneDeep(vtt))
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Try to get locale - priority: API response > store lookup > null
|
|
660
|
+
let locale = vtt.locale || null
|
|
661
|
+
|
|
662
|
+
// If no locale from API but we have locale_id, try store lookup
|
|
663
|
+
if (!locale && vtt.locale_id && this.$store?.getters?.['locales/getById']) {
|
|
664
|
+
locale = this.$store.getters['locales/getById'](vtt.locale_id)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
...vtt,
|
|
669
|
+
locale: locale
|
|
670
|
+
}
|
|
671
|
+
})
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Get single linked caption (backward compatibility)
|
|
676
|
+
* @param {Object} file - The video file object
|
|
677
|
+
* @returns {Object|null} First VTT file found
|
|
678
|
+
*/
|
|
679
|
+
getLinkedCaptions(file) {
|
|
680
|
+
// Check to see if the video source has a linked asset and it's a vtt file
|
|
681
|
+
// Prefer the most recently linked VTT (last match), not the oldest.
|
|
682
|
+
const linkedAssets = _.get(file, 'asset.linked_assets', [])
|
|
683
|
+
const linkedCaption = _.findLast(linkedAssets, function (f) {
|
|
684
|
+
const ext = _.get(f, 'asset.metadata.extension', '').toLowerCase()
|
|
685
|
+
const mime = _.get(f, 'asset.metadata.mime', '').toLowerCase()
|
|
686
|
+
const name = _.get(f, 'asset.name', '').toLowerCase()
|
|
687
|
+
|
|
688
|
+
return (
|
|
689
|
+
ext === 'vtt' ||
|
|
690
|
+
mime === 'text/vtt' ||
|
|
691
|
+
name.endsWith('.vtt')
|
|
692
|
+
)
|
|
693
|
+
})
|
|
694
|
+
|
|
409
695
|
if (linkedCaption) {
|
|
410
696
|
const foundAsset = this.block.assets.find((a) => {
|
|
411
697
|
return a.file_asset_id === linkedCaption.file_asset_id
|
|
@@ -421,6 +707,69 @@ export default {
|
|
|
421
707
|
return linkedCaption || null
|
|
422
708
|
},
|
|
423
709
|
|
|
710
|
+
/**
|
|
711
|
+
* TODO:find a better place for this to live
|
|
712
|
+
* Get human-readable language label for a language code
|
|
713
|
+
* @param {string} langCode - ISO language code (e.g., 'en-US')
|
|
714
|
+
* @returns {string} Language name
|
|
715
|
+
*/
|
|
716
|
+
getLabelForLanguage(langCode) {
|
|
717
|
+
// Map common language codes to display names
|
|
718
|
+
const labels = {
|
|
719
|
+
'en': 'English',
|
|
720
|
+
'en-US': 'English',
|
|
721
|
+
'en-GB': 'English (UK)',
|
|
722
|
+
'es': 'Spanish',
|
|
723
|
+
'es-ES': 'Spanish',
|
|
724
|
+
'es-MX': 'Spanish (Mexico)',
|
|
725
|
+
'fr': 'French',
|
|
726
|
+
'fr-FR': 'French',
|
|
727
|
+
'fr-CA': 'French (Canada)',
|
|
728
|
+
'de': 'German',
|
|
729
|
+
'de-DE': 'German',
|
|
730
|
+
'it': 'Italian',
|
|
731
|
+
'it-IT': 'Italian',
|
|
732
|
+
'pt': 'Portuguese',
|
|
733
|
+
'pt-BR': 'Portuguese (Brazil)',
|
|
734
|
+
'pt-PT': 'Portuguese (Portugal)',
|
|
735
|
+
'zh': 'Chinese',
|
|
736
|
+
'zh-CN': 'Chinese (Simplified)',
|
|
737
|
+
'zh-TW': 'Chinese (Traditional)',
|
|
738
|
+
'ja': 'Japanese',
|
|
739
|
+
'ja-JP': 'Japanese',
|
|
740
|
+
'ko': 'Korean',
|
|
741
|
+
'ko-KR': 'Korean',
|
|
742
|
+
'ru': 'Russian',
|
|
743
|
+
'ru-RU': 'Russian',
|
|
744
|
+
'ar': 'Arabic',
|
|
745
|
+
'ar-SA': 'Arabic',
|
|
746
|
+
'hi': 'Hindi',
|
|
747
|
+
'hi-IN': 'Hindi',
|
|
748
|
+
}
|
|
749
|
+
return labels[langCode] || langCode
|
|
750
|
+
},
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Check if a caption language is allowed based on course source/target languages
|
|
754
|
+
* Since allowedCaptionLocales already contains both full codes (en-us) and base codes (en),
|
|
755
|
+
* we just need to check if the language or its base is in the Set.
|
|
756
|
+
* @param {string} lang - The language code to check (e.g., 'en-US', 'PT-BR')
|
|
757
|
+
* @returns {boolean} - True if the language is allowed
|
|
758
|
+
*/
|
|
759
|
+
isAllowedCaptionLanguage(lang) {
|
|
760
|
+
if (!lang) {
|
|
761
|
+
// Allow tracks with no language set (legacy/default tracks)
|
|
762
|
+
return true
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const langLower = lang.toLowerCase()
|
|
766
|
+
const langBase = langLower.split('-')[0]
|
|
767
|
+
|
|
768
|
+
// Check if the full language code or its base is in allowedCaptionLocales
|
|
769
|
+
return this.allowedCaptionLocales.has(langLower) ||
|
|
770
|
+
this.allowedCaptionLocales.has(langBase)
|
|
771
|
+
},
|
|
772
|
+
|
|
424
773
|
/**
|
|
425
774
|
* Check if the given text has words, omitting HTML tags and HTML entities
|
|
426
775
|
* @param {string} text - The text to check
|
|
@@ -165,7 +165,9 @@ export default {
|
|
|
165
165
|
this.block.metadata.config = {}
|
|
166
166
|
}
|
|
167
167
|
if (_.isEmpty(this.block.metadata.config.title)) {
|
|
168
|
-
this.block.metadata.config.title =
|
|
168
|
+
this.block.metadata.config.title = this.$t(
|
|
169
|
+
'windward.core.components.settings.accordion.accordion'
|
|
170
|
+
)
|
|
169
171
|
}
|
|
170
172
|
if (!_.isBoolean(this.block.metadata.config.display_title)) {
|
|
171
173
|
this.$set(this.block.metadata.config, 'display_title', true)
|
|
@@ -183,21 +185,7 @@ export default {
|
|
|
183
185
|
this.block.metadata.config.editOnContentItem = false
|
|
184
186
|
}
|
|
185
187
|
if (_.isEmpty(this.block.metadata.config.items)) {
|
|
186
|
-
const defaultObject = {
|
|
187
|
-
header: '',
|
|
188
|
-
expand: false,
|
|
189
|
-
content: '',
|
|
190
|
-
fileConfig: {
|
|
191
|
-
display: {
|
|
192
|
-
width: 100,
|
|
193
|
-
margin: '',
|
|
194
|
-
padding: '',
|
|
195
|
-
},
|
|
196
|
-
hideBackground: true,
|
|
197
|
-
},
|
|
198
|
-
}
|
|
199
188
|
this.block.metadata.config.items = []
|
|
200
|
-
this.block.metadata.config.items.push(defaultObject)
|
|
201
189
|
}
|
|
202
190
|
this.block.body = this.$t(
|
|
203
191
|
'windward.core.shared.content_blocks.title.accordion'
|