|
8 | 8 | @closePanel="onClose" |
9 | 9 | > |
10 | 10 | <template #header> |
11 | | - <h1 class="side-panel-title">{{ getPanelTitle() }}</h1> |
| 11 | + <h1 class="side-panel-title">{{ publishChannel$() }}</h1> |
12 | 12 | </template> |
13 | 13 |
|
14 | 14 | <template #default> |
|
49 | 49 | <div class="form-section"> |
50 | 50 | <KTextbox |
51 | 51 | v-model="version_notes" |
| 52 | + showInvalidText |
52 | 53 | :label="versionDescriptionLabel$()" |
53 | | - :invalid="version_notes.length === 0" |
54 | | - :invalidText="'Version notes are required'" |
55 | | - :showInvalidText="showVersionNotesInvalidText" |
| 54 | + :invalid="isVersionNotesBlurred && !isVersionNotesValid" |
| 55 | + :invalidText="versionNotesRequiredMessage$()" |
56 | 56 | textArea |
57 | 57 | :maxlength="250" |
58 | 58 | :appearanceOverrides="{ maxWidth: 'none' }" |
59 | | - @blur="onVersionNotesBlur" |
| 59 | + @blur="isVersionNotesBlurred = true" |
60 | 60 | /> |
61 | 61 | </div> |
62 | 62 | <!-- Language selector --> |
| 63 | + <KCircularLoader v-if="isChannelLanguageLoading" /> |
63 | 64 | <div |
64 | | - v-if="showLanguageDropdown" |
| 65 | + v-else-if="showLanguageDropdown" |
65 | 66 | class="form-section" |
66 | 67 | > |
67 | 68 | <KSelect |
68 | | - v-model="language" |
| 69 | + v-model="newChannelLanguage" |
69 | 70 | :label="languageLabel$()" |
70 | | - :invalid="showLanguageInvalidText" |
| 71 | + :invalid="isLanguageSelectBlurred && !isNewLanguageValid" |
71 | 72 | :invalidText="languageRequiredMessage$()" |
72 | | - :options="languages" |
73 | | - @change="onLanguageChange" |
| 73 | + :options="languageOptions" |
| 74 | + @blur="isLanguageSelectBlurred = true" |
74 | 75 | /> |
75 | 76 | </div> |
76 | 77 | </div> |
|
152 | 153 |
|
153 | 154 | <script> |
154 | 155 |
|
155 | | - import { ref, computed, getCurrentInstance, onMounted } from 'vue'; |
| 156 | + import { ref, computed, getCurrentInstance } from 'vue'; |
156 | 157 | import SidePanelModal from 'shared/views/SidePanelModal'; |
157 | 158 | import { Channel, CommunityLibrarySubmission } from 'shared/data/resources'; |
158 | 159 | import { forceServerSync } from 'shared/data/serverSync'; |
|
173 | 174 |
|
174 | 175 | const mode = ref(PublishModes.LIVE); |
175 | 176 | const version_notes = ref(''); |
| 177 | + const newChannelLanguage = ref({}); |
| 178 | + const languageOptions = ref(null); |
| 179 | +
|
| 180 | + const isChannelLanguageLoading = ref(false); |
176 | 181 | const submitting = ref(false); |
177 | | - const language = ref({}); |
178 | | - const showLanguageInvalidText = ref(false); |
179 | | - const showVersionNotesInvalidText = ref(false); // lazy validation |
180 | | - const channelLanguages = ref([]); |
181 | | - const channelLanguageExists = ref(true); |
| 182 | + const isVersionNotesBlurred = ref(false); |
| 183 | + const isLanguageSelectBlurred = ref(false); |
| 184 | + const isCurrentChannelLanguageValid = ref(false); |
182 | 185 |
|
183 | 186 | const instance = getCurrentInstance(); |
184 | 187 | const store = instance.proxy.$store; |
|
205 | 208 | cancelAction$, |
206 | 209 | languageLabel$, |
207 | 210 | languageRequiredMessage$, |
| 211 | + versionNotesRequiredMessage$, |
208 | 212 | } = communityChannelsStrings; |
209 | 213 |
|
210 | 214 | const currentChannel = computed(() => store.getters['currentChannel/currentChannel']); |
|
224 | 228 | const isPrivateChannel = currentChannel.value.public === false; |
225 | 229 | const isFirstPublish = currentChannel.value.version === 0; |
226 | 230 |
|
227 | | - return ( |
228 | | - ((isCheffedChannel || isPrivateChannel) && isFirstPublish) || !channelLanguageExists.value |
229 | | - ); |
230 | | - }); |
231 | | -
|
232 | | - const languages = computed(() => { |
233 | | - if (!currentChannel.value) return []; |
234 | | - return filterLanguages(l => channelLanguages.value.includes(l.id)); |
235 | | - }); |
236 | | -
|
237 | | - const defaultLanguage = computed(() => { |
238 | | - if (!currentChannel.value?.language) return {}; |
239 | | - const channelLang = filterLanguages(l => l.id === currentChannel.value.language)[0]; |
240 | | - return languages.value.some(lang => lang.value === channelLang?.value) ? channelLang : {}; |
| 231 | + if (isFirstPublish) { |
| 232 | + return isCheffedChannel || isPrivateChannel; |
| 233 | + } |
| 234 | + return !isCurrentChannelLanguageValid.value; |
241 | 235 | }); |
242 | 236 |
|
243 | | - const isLanguageValid = computed(() => { |
244 | | - return Object.keys(language.value).length > 0; |
| 237 | + const isNewLanguageValid = computed(() => { |
| 238 | + // If language selector is not shown do not prevent submission |
| 239 | + if (!showLanguageDropdown.value) return true; |
| 240 | + return Object.keys(newChannelLanguage.value).length > 0; |
245 | 241 | }); |
246 | 242 |
|
247 | 243 | const isVersionNotesValid = computed(() => { |
|
251 | 247 | // Validate the version and language for live mode |
252 | 248 | const isFormValid = computed(() => { |
253 | 249 | if (mode.value === PublishModes.LIVE) { |
254 | | - const versionNotesValid = isVersionNotesValid.value; |
255 | | - const languageValid = showLanguageDropdown.value ? isLanguageValid.value : true; |
256 | | - return versionNotesValid && languageValid; |
| 250 | + return isVersionNotesValid.value && isNewLanguageValid.value; |
257 | 251 | } |
258 | 252 | return true; |
259 | 253 | }); |
|
262 | 256 | return mode.value === PublishModes.DRAFT ? saveDraft$() : publishAction$(); |
263 | 257 | }); |
264 | 258 |
|
265 | | - const filterLanguages = filterFn => { |
266 | | - return LanguagesList.filter(filterFn).map(l => ({ |
| 259 | + const getLanguagesOptions = languages => { |
| 260 | + return mapLanguagesOptions(LanguagesList.filter(lang => languages.includes(lang.id))); |
| 261 | + }; |
| 262 | + const mapLanguagesOptions = languages => { |
| 263 | + return languages.map(l => ({ |
267 | 264 | value: l.id, |
268 | 265 | label: l.native_name, |
269 | 266 | })); |
270 | 267 | }; |
271 | 268 |
|
272 | | - // Validate the selected language when it changes |
273 | | - const validateSelectedLanguage = () => { |
274 | | - if (Object.keys(language.value).length > 0 && languages.value.length > 0) { |
275 | | - const isValidLanguage = languages.value.some(lang => lang.value === language.value.value); |
276 | | - if (!isValidLanguage) { |
277 | | - language.value = {}; |
| 269 | + /** |
| 270 | + * Makes sure that the channel.language is consistent with the channel resources languages |
| 271 | + * If not, show the language selector for the user to select a valid language for the channel |
| 272 | + */ |
| 273 | + const _auditChannelLanguage = async () => { |
| 274 | + if (!currentChannel.value) { |
| 275 | + return; |
| 276 | + } |
| 277 | +
|
| 278 | + const currentChannelLanguage = currentChannel.value.language; |
| 279 | + // If the current `channel.language` exists among its resources languages, |
| 280 | + // the channel language is valid |
| 281 | + isCurrentChannelLanguageValid.value = await channelLanguageExistsInResources(); |
| 282 | + const channelLanguageOption = getLanguagesOptions([currentChannelLanguage])[0]; |
| 283 | +
|
| 284 | + if (!isCurrentChannelLanguageValid.value || !channelLanguageOption) { |
| 285 | + // If not valid, or it doesn't exist in our LanguagesList, load resources languages |
| 286 | + const resourcesLanguages = await getLanguagesInChannelResources(); |
| 287 | + if (resourcesLanguages.length > 0) { |
| 288 | + languageOptions.value = getLanguagesOptions(resourcesLanguages); |
| 289 | + return; |
278 | 290 | } |
279 | 291 | } |
| 292 | +
|
| 293 | + // if not found in LanguagesList and no resources languages found, set the whole |
| 294 | + // LanguagesList as options |
| 295 | + if (!channelLanguageOption) { |
| 296 | + languageOptions.value = mapLanguagesOptions(LanguagesList); |
| 297 | + return; |
| 298 | + } |
| 299 | + // In any other case, just set the channel language as the only option |
| 300 | + languageOptions.value = [channelLanguageOption]; |
| 301 | + newChannelLanguage.value = channelLanguageOption; |
280 | 302 | }; |
281 | 303 |
|
282 | | - onMounted(async () => { |
283 | | - if (currentChannel.value) { |
284 | | - const exists = await channelLanguageExistsInResources(); |
285 | | - channelLanguageExists.value = exists; |
| 304 | + const auditChannelLanguage = async () => { |
| 305 | + isChannelLanguageLoading.value = true; |
| 306 | + await _auditChannelLanguage(); |
| 307 | + isChannelLanguageLoading.value = false; |
| 308 | + }; |
286 | 309 |
|
287 | | - if (!exists) { |
288 | | - const languages = await getLanguagesInChannelResources(); |
289 | | - channelLanguages.value = languages.length ? languages : [currentChannel.value.language]; |
290 | | - language.value = defaultLanguage.value; |
291 | | - validateSelectedLanguage(); |
292 | | - } else { |
293 | | - channelLanguages.value = [currentChannel.value.language]; |
294 | | - language.value = defaultLanguage.value; |
295 | | - validateSelectedLanguage(); |
296 | | - } |
297 | | - } |
298 | | - }); |
| 310 | + auditChannelLanguage(); |
299 | 311 |
|
300 | 312 | const onClose = () => { |
301 | 313 | if (!submitting.value) emit('close'); |
302 | 314 | }; |
303 | 315 |
|
| 316 | + const checkResubmitToCommunityLibrary = async () => { |
| 317 | + if (mode.value !== PublishModes.LIVE) { |
| 318 | + return; |
| 319 | + } |
| 320 | +
|
| 321 | + try { |
| 322 | + const response = await CommunityLibrarySubmission.fetchCollection({ |
| 323 | + channel: currentChannel.value.id, |
| 324 | + max_results: 1, |
| 325 | + }); |
| 326 | +
|
| 327 | + const submissions = response?.results || []; |
| 328 | +
|
| 329 | + if (submissions.length > 0) { |
| 330 | + const latestSubmission = submissions[0]; |
| 331 | + emit('showResubmitCommunityLibraryModal', { |
| 332 | + channel: { ...currentChannel.value }, |
| 333 | + latestSubmissionVersion: latestSubmission.channel_version, |
| 334 | + }); |
| 335 | + } |
| 336 | + } catch (error) { |
| 337 | + // Log the error but do not block the publish flow |
| 338 | + logging.error(error); |
| 339 | + } |
| 340 | + }; |
| 341 | +
|
304 | 342 | const submit = async () => { |
305 | | - // Validate form before submission |
306 | | - if (!validate()) { |
| 343 | + // Should not get here if the form is invalid, but just in case |
| 344 | + if (!isFormValid.value) { |
307 | 345 | return; |
308 | 346 | } |
309 | 347 |
|
|
319 | 357 | await Channel.publishDraft(currentChannel.value.id, { use_staging_tree: false }); |
320 | 358 | emit('close'); |
321 | 359 | } else { |
322 | | - if ( |
323 | | - language.value && |
324 | | - language.value.value && |
325 | | - language.value.value !== currentChannel.value?.language |
326 | | - ) { |
| 360 | + if (newChannelLanguage.value.value !== currentChannel.value.language) { |
327 | 361 | await store.dispatch('channel/updateChannel', { |
328 | 362 | id: currentChannel.value.id, |
329 | | - language: language.value.value, |
| 363 | + language: newChannelLanguage.value.value, |
330 | 364 | }); |
331 | 365 | } |
332 | 366 |
|
333 | 367 | await Channel.publish(currentChannel.value.id, version_notes.value); |
334 | 368 |
|
335 | | - if (mode.value === PublishModes.LIVE) { |
336 | | - try { |
337 | | - const response = await CommunityLibrarySubmission.fetchCollection({ |
338 | | - channel: currentChannel.value.id, |
339 | | - max_results: 1, |
340 | | - }); |
341 | | -
|
342 | | - const submissions = response?.results || []; |
343 | | -
|
344 | | - if (submissions.length > 0) { |
345 | | - const latestSubmission = submissions[0]; |
346 | | - emit('showResubmitCommunityLibraryModal', { |
347 | | - channel: { ...currentChannel.value }, |
348 | | - latestSubmissionVersion: latestSubmission.channel_version, |
349 | | - }); |
350 | | - } |
351 | | - } catch (error) { |
352 | | - logging.error(error); |
353 | | - } |
354 | | - } |
| 369 | + await checkResubmitToCommunityLibrary(); |
355 | 370 |
|
356 | 371 | emit('close'); |
357 | 372 | } |
|
362 | 377 | } |
363 | 378 | }; |
364 | 379 |
|
365 | | - const getPanelTitle = () => { |
366 | | - return publishChannel$(); |
367 | | - }; |
368 | | -
|
369 | | - const onLanguageChange = () => { |
370 | | - showLanguageInvalidText.value = !isLanguageValid.value; |
371 | | - validateSelectedLanguage(); // Ensure language is always valid |
372 | | - }; |
373 | | -
|
374 | | - const onVersionNotesBlur = () => { |
375 | | - showVersionNotesInvalidText.value = !isVersionNotesValid.value; |
376 | | - }; |
377 | | -
|
378 | | - const validate = () => { |
379 | | - if (mode.value === PublishModes.DRAFT) { |
380 | | - // For draft mode, no validation is required |
381 | | - return true; |
382 | | - } else { |
383 | | - // For live mode, validate version notes and language |
384 | | - showVersionNotesInvalidText.value = !isVersionNotesValid.value; |
385 | | - showLanguageInvalidText.value = !isLanguageValid.value; |
386 | | - return !showVersionNotesInvalidText.value && !showLanguageInvalidText.value; |
387 | | - } |
388 | | - }; |
389 | | -
|
390 | 380 | return { |
| 381 | + isChannelLanguageLoading, |
391 | 382 | PublishModes, |
392 | 383 | mode, |
393 | 384 | version_notes, |
394 | 385 | submitting, |
395 | | - language, |
396 | | - showLanguageInvalidText, |
397 | | - showVersionNotesInvalidText, |
| 386 | + languageOptions, |
| 387 | + newChannelLanguage, |
| 388 | + isVersionNotesBlurred, |
| 389 | + isLanguageSelectBlurred, |
398 | 390 |
|
399 | 391 | currentChannel, |
400 | 392 | incompleteResourcesCount, |
401 | 393 | showLanguageDropdown, |
402 | | - languages, |
403 | 394 | isFormValid, |
| 395 | + isNewLanguageValid, |
| 396 | + isVersionNotesValid, |
404 | 397 | submitText, |
405 | 398 |
|
406 | 399 | modeLive$, |
407 | 400 | modeDraft$, |
| 401 | + publishChannel$, |
408 | 402 | versionNotesLabel$, |
409 | 403 | modeLiveDescription$, |
410 | 404 | modeDraftDescription$, |
|
416 | 410 | cancelAction$, |
417 | 411 | languageLabel$, |
418 | 412 | languageRequiredMessage$, |
| 413 | + versionNotesRequiredMessage$, |
419 | 414 |
|
420 | 415 | onClose, |
421 | 416 | submit, |
422 | | - getPanelTitle, |
423 | | - onLanguageChange, |
424 | | - onVersionNotesBlur, |
425 | 417 | }; |
426 | 418 | }, |
427 | 419 |
|
|
0 commit comments