From 1305e2e454022728eb5de12d3b78031bd47fb251 Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Thu, 25 Jun 2026 22:39:30 +0100 Subject: [PATCH] feat(dashboard): add view course site actions and publish flow (#690) Add View course site and Publish course options to org course context menus. Replace the course header View as student button with View Course Site and a companion overflow menu that includes View as student plus list actions. Show a publish prompt modal for unpublished courses and support #publish deep links in course settings. --- .../course/components/card-dropdown.svelte | 83 +--------- .../course-context-menu-content.svelte | 155 ++++++++++++++++++ .../course/components/course-header.svelte | 83 ++++++++-- .../course/components/course-list-row.svelte | 82 +++------ .../view-course-site-unpublished-modal.svelte | 66 ++++++++ .../lib/features/course/pages/settings.svelte | 2 +- .../features/course/utils/publish-course.ts | 32 ++++ .../src/lib/utils/translations/da.json | 11 +- .../src/lib/utils/translations/de.json | 11 +- .../src/lib/utils/translations/en.json | 11 +- .../src/lib/utils/translations/es.json | 11 +- .../src/lib/utils/translations/fr.json | 11 +- .../src/lib/utils/translations/hi.json | 11 +- .../src/lib/utils/translations/pl.json | 11 +- .../src/lib/utils/translations/pt.json | 11 +- .../src/lib/utils/translations/ru.json | 11 +- .../src/lib/utils/translations/vi.json | 11 +- 17 files changed, 449 insertions(+), 164 deletions(-) create mode 100644 apps/dashboard/src/lib/features/course/components/course-context-menu-content.svelte create mode 100644 apps/dashboard/src/lib/features/course/components/view-course-site-unpublished-modal.svelte create mode 100644 apps/dashboard/src/lib/features/course/utils/publish-course.ts diff --git a/apps/dashboard/src/lib/features/course/components/card-dropdown.svelte b/apps/dashboard/src/lib/features/course/components/card-dropdown.svelte index 4f48e376..e777dda8 100644 --- a/apps/dashboard/src/lib/features/course/components/card-dropdown.svelte +++ b/apps/dashboard/src/lib/features/course/components/card-dropdown.svelte @@ -1,16 +1,8 @@ {#if lmsPublicQuickOnly ? showPublicCourseLinks : true} @@ -84,41 +39,7 @@ - {#if lmsPublicQuickOnly} - - - {$t('courses.course_card.context_menu.open_public_course')} - - void handleCopyCourseUrl()}> - - {$t('courses.course_card.context_menu.copy_course_url')} - - {:else} - {#if showPublicCourseLinks} - - - {$t('courses.course_card.context_menu.open_public_course')} - - void handleCopyCourseUrl()}> - - {$t('courses.course_card.context_menu.copy_course_url')} - - - {/if} - - {$t('courses.course_card.context_menu.clone')} - - - {$t('courses.course_card.context_menu.share')} - - - {$t('courses.course_card.context_menu.invite')} - - - - {$t('courses.course_card.context_menu.delete')} - - {/if} + {/if} diff --git a/apps/dashboard/src/lib/features/course/components/course-context-menu-content.svelte b/apps/dashboard/src/lib/features/course/components/course-context-menu-content.svelte new file mode 100644 index 00000000..dfd90821 --- /dev/null +++ b/apps/dashboard/src/lib/features/course/components/course-context-menu-content.svelte @@ -0,0 +1,155 @@ + + +{#if lmsPublicQuickOnly} + + + {$t('courses.course_card.context_menu.view_course_site')} + + void handleCopyCourseUrl()}> + + {$t('courses.course_card.context_menu.copy_course_url')} + +{:else} + {#if includeViewAsStudent} + onViewAsStudent?.()}> + + {$t('course.header.view_as_student')} + + + {/if} + + {#if isPublished} + + + {$t('courses.course_card.context_menu.view_course_site')} + + {#if showPublicCourseLinks} + void handleCopyCourseUrl()}> + + {$t('courses.course_card.context_menu.copy_course_url')} + + {/if} + {#if !hideOrgActions || includeOpen} + + {/if} + {:else} + + {$t('courses.course_card.context_menu.publish_course')} + + {#if !hideOrgActions || includeOpen} + + {/if} + {/if} + + {#if includeOpen} + + {$t('courses.course_card.context_menu.open')} + + {/if} + + {#if !hideOrgActions} + + {$t('courses.course_card.context_menu.clone')} + + + {$t('courses.course_card.context_menu.share')} + + + {$t('courses.course_card.context_menu.invite')} + + + + {$t('courses.course_card.context_menu.delete')} + + {/if} +{/if} diff --git a/apps/dashboard/src/lib/features/course/components/course-header.svelte b/apps/dashboard/src/lib/features/course/components/course-header.svelte index 652af5da..35150164 100644 --- a/apps/dashboard/src/lib/features/course/components/course-header.svelte +++ b/apps/dashboard/src/lib/features/course/components/course-header.svelte @@ -1,7 +1,10 @@
+ {/if} @@ -78,16 +104,47 @@ {#if !$isStudentExperience} - + + + + + {#snippet child({ props })} + + {/snippet} + + + {#if courseApi.course} + (viewAsStudentOpen = true)} + /> + {/if} + + + {/if}
@@ -98,3 +155,5 @@ courseSlug={courseApi.course?.slug} currentOrgDomain={$currentOrgDomain} /> + + diff --git a/apps/dashboard/src/lib/features/course/components/course-list-row.svelte b/apps/dashboard/src/lib/features/course/components/course-list-row.svelte index 5c6c952f..a75a1ad7 100644 --- a/apps/dashboard/src/lib/features/course/components/course-list-row.svelte +++ b/apps/dashboard/src/lib/features/course/components/course-list-row.svelte @@ -10,14 +10,10 @@ import { ContentType } from '@cio/utils/constants/content'; import { Image } from '$features/ui'; import { t } from '$lib/utils/functions/translations'; - import { copyCourseModal, deleteCourseModal } from '$features/course/utils/store'; - import { copyPublicCoursePageUrl, openCoursePreview } from '$features/course/utils/course-preview'; - import { currentOrgDomain } from '$lib/utils/store/org'; import { buildCoursePlaceholderAvatarUrl } from '$features/course/utils/course-list-row-utils'; import CourseContentIcon from './course-content-icon.svelte'; import CoursePublicBadge from './course-public-badge.svelte'; - import ExternalLinkIcon from '@lucide/svelte/icons/external-link'; - import CopyIcon from '@lucide/svelte/icons/copy'; + import CourseContextMenuContent from './course-context-menu-content.svelte'; interface Tag { id: string; @@ -133,30 +129,6 @@ } goto(resolve(`/courses/${id}`, {})); } - - function handleOpen(e: MouseEvent) { - e.stopPropagation(); - goto(resolve(`/courses/${id}`, {})); - } - - function handleClone(e: MouseEvent) { - e.stopPropagation(); - $copyCourseModal.open = true; - $copyCourseModal.id = id; - $copyCourseModal.title = title; - $copyCourseModal.description = description; - } - - function handleDelete(e: MouseEvent) { - e.stopPropagation(); - $deleteCourseModal.open = true; - $deleteCourseModal.id = id; - $deleteCourseModal.title = title; - } - - async function handleCopyCourseUrl() { - await copyPublicCoursePageUrl(slug, $currentOrgDomain); - } - {#if showPublicCourseLinks} - - openCoursePreview({ - courseId: id, - courseSlug: slug, - currentOrgDomain: $currentOrgDomain - })} - > - - {$t('courses.course_card.context_menu.open_public_course')} - - void handleCopyCourseUrl()}> - - {$t('courses.course_card.context_menu.copy_course_url')} - - {/if} - {#if !isLMS} - {#if showPublicCourseLinks} - - {/if} - - {$t('courses.course_card.context_menu.open')} - - - {$t('courses.course_card.context_menu.clone')} - - - - {$t('courses.course_card.context_menu.delete')} - + {#if isLMS} + + {:else} + {/if} diff --git a/apps/dashboard/src/lib/features/course/components/view-course-site-unpublished-modal.svelte b/apps/dashboard/src/lib/features/course/components/view-course-site-unpublished-modal.svelte new file mode 100644 index 00000000..81b45306 --- /dev/null +++ b/apps/dashboard/src/lib/features/course/components/view-course-site-unpublished-modal.svelte @@ -0,0 +1,66 @@ + + + { + if (!isOpen && !isPublishing) { + open = false; + } + }} +> + + + {$t('course.view_course_site.unpublished_title')} + {$t('course.view_course_site.unpublished_description')} + + + + + + + + diff --git a/apps/dashboard/src/lib/features/course/pages/settings.svelte b/apps/dashboard/src/lib/features/course/pages/settings.svelte index d918bfcd..c6c4a594 100644 --- a/apps/dashboard/src/lib/features/course/pages/settings.svelte +++ b/apps/dashboard/src/lib/features/course/pages/settings.svelte @@ -794,7 +794,7 @@ - + {$t('course.navItem.settings.publish')} {$t('course.navItem.settings.determines')} diff --git a/apps/dashboard/src/lib/features/course/utils/publish-course.ts b/apps/dashboard/src/lib/features/course/utils/publish-course.ts new file mode 100644 index 00000000..aba34016 --- /dev/null +++ b/apps/dashboard/src/lib/features/course/utils/publish-course.ts @@ -0,0 +1,32 @@ +import { generateSlug } from '@cio/utils/functions'; +import { isObject } from '$lib/utils/functions/isObject'; +import { courseApi } from '$features/course/api'; +import type { Course } from '$features/course/utils/types'; + +export async function publishCourse(course: Course): Promise { + if (!course?.id) { + return false; + } + + let slug = course.slug?.trim() ? course.slug : undefined; + if (!slug) { + slug = generateSlug(course.title, { appendTimestamp: true }); + } + + const metadataPayload = { + ...(isObject(course.metadata) ? course.metadata : {}), + allowNewStudent: true + }; + + const result = await courseApi.update( + course.id, + { + isPublished: true, + slug, + metadata: metadataPayload + }, + { showSuccessToast: true } + ); + + return !!result; +} diff --git a/apps/dashboard/src/lib/utils/translations/da.json b/apps/dashboard/src/lib/utils/translations/da.json index db0fcc7f..bde2ec6b 100644 --- a/apps/dashboard/src/lib/utils/translations/da.json +++ b/apps/dashboard/src/lib/utils/translations/da.json @@ -2234,7 +2234,14 @@ "header": { "preview": "Preview course", "preview_missing_slug": "Generate and save a course slug before previewing.", - "view_as_student": "Se som studerende" + "view_as_student": "Se som studerende", + "view_course_site": "Se kursusside" + }, + "view_course_site": { + "unpublished_title": "Udgiv dit kursus", + "unpublished_description": "Du bør udgive dette kursus, før du kan se sitet", + "cancel": "Annuller", + "publish_course": "Udgiv kursus" }, "sidebar": { "back_to_course": "Tilbage til kurset", @@ -2420,6 +2427,8 @@ "delete": "Slet", "open": "Åbn", "open_public_course": "Åbn offentligt kursus", + "view_course_site": "Se kursusside", + "publish_course": "Udgiv kursus", "copy_course_url": "Kopiér kursus-URL" }, "due_date": "Forfalder", diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json index 741a5230..05522897 100644 --- a/apps/dashboard/src/lib/utils/translations/de.json +++ b/apps/dashboard/src/lib/utils/translations/de.json @@ -2234,7 +2234,14 @@ "header": { "preview": "Preview course", "preview_missing_slug": "Generate and save a course slug before previewing.", - "view_as_student": "Als Student anzeigen" + "view_as_student": "Als Student anzeigen", + "view_course_site": "Kursseite anzeigen" + }, + "view_course_site": { + "unpublished_title": "Kurs veröffentlichen", + "unpublished_description": "Sie sollten diesen Kurs veröffentlichen, bevor Sie die Seite ansehen können", + "cancel": "Abbrechen", + "publish_course": "Kurs veröffentlichen" }, "sidebar": { "back_to_course": "Zurück zum Kurs", @@ -2420,6 +2427,8 @@ "delete": "Löschen", "open": "Offen", "open_public_course": "Öffentlichen Kurs öffnen", + "view_course_site": "Kursseite anzeigen", + "publish_course": "Kurs veröffentlichen", "copy_course_url": "Kurs-URL kopieren" }, "due_date": "Fällig", diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json index 6d545d69..2473ed00 100644 --- a/apps/dashboard/src/lib/utils/translations/en.json +++ b/apps/dashboard/src/lib/utils/translations/en.json @@ -1970,6 +1970,8 @@ "invite": "Invite", "delete": "Delete", "open_public_course": "Open public course", + "view_course_site": "View course site", + "publish_course": "Publish course", "copy_course_url": "Copy course URL" }, "error_message": "An error occurred.", @@ -2147,7 +2149,14 @@ "header": { "preview": "Preview course", "preview_missing_slug": "Generate and save a course slug before previewing.", - "view_as_student": "View as student" + "view_as_student": "View as student", + "view_course_site": "View Course Site" + }, + "view_course_site": { + "unpublished_title": "Publish your course", + "unpublished_description": "You should publish this course before you can see the site", + "cancel": "Cancel", + "publish_course": "Publish Course" }, "view_as_student": { "title": "This will redirect you to the Student LMS.", diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json index 85404798..fc8e67d0 100644 --- a/apps/dashboard/src/lib/utils/translations/es.json +++ b/apps/dashboard/src/lib/utils/translations/es.json @@ -2234,7 +2234,14 @@ "header": { "preview": "Preview course", "preview_missing_slug": "Generate and save a course slug before previewing.", - "view_as_student": "Ver como estudiante" + "view_as_student": "Ver como estudiante", + "view_course_site": "Ver sitio del curso" + }, + "view_course_site": { + "unpublished_title": "Publica tu curso", + "unpublished_description": "Debes publicar este curso antes de poder ver el sitio", + "cancel": "Cancelar", + "publish_course": "Publicar curso" }, "sidebar": { "back_to_course": "volver al curso", @@ -2420,6 +2427,8 @@ "delete": "Eliminar", "open": "Abierto", "open_public_course": "Abrir curso público", + "view_course_site": "Ver sitio del curso", + "publish_course": "Publicar curso", "copy_course_url": "Copiar URL del curso" }, "due_date": "debido", diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json index 6a8e84d0..ebf30ec4 100644 --- a/apps/dashboard/src/lib/utils/translations/fr.json +++ b/apps/dashboard/src/lib/utils/translations/fr.json @@ -2234,7 +2234,14 @@ "header": { "preview": "Preview course", "preview_missing_slug": "Generate and save a course slug before previewing.", - "view_as_student": "Afficher en tant qu'étudiant" + "view_as_student": "Afficher en tant qu'étudiant", + "view_course_site": "Voir le site du cours" + }, + "view_course_site": { + "unpublished_title": "Publiez votre cours", + "unpublished_description": "Vous devez publier ce cours avant de pouvoir voir le site", + "cancel": "Annuler", + "publish_course": "Publier le cours" }, "sidebar": { "back_to_course": "Retour au cours", @@ -2420,6 +2427,8 @@ "delete": "Supprimer", "open": "Ouvert", "open_public_course": "Ouvrir le cours public", + "view_course_site": "Voir le site du cours", + "publish_course": "Publier le cours", "copy_course_url": "Copier l’URL du cours" }, "due_date": "Due", diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json index 32034545..e1aa0e41 100644 --- a/apps/dashboard/src/lib/utils/translations/hi.json +++ b/apps/dashboard/src/lib/utils/translations/hi.json @@ -2234,7 +2234,14 @@ "header": { "preview": "Preview course", "preview_missing_slug": "Generate and save a course slug before previewing.", - "view_as_student": "विद्यार्थी के रूप में देखें" + "view_as_student": "विद्यार्थी के रूप में देखें", + "view_course_site": "कोर्स साइट देखें" + }, + "view_course_site": { + "unpublished_title": "अपना कोर्स प्रकाशित करें", + "unpublished_description": "साइट देखने से पहले आपको इस कोर्स को प्रकाशित करना चाहिए", + "cancel": "रद्द करें", + "publish_course": "कोर्स प्रकाशित करें" }, "sidebar": { "back_to_course": "पाठ्यक्रम पर वापस जाएँ", @@ -2420,6 +2427,8 @@ "delete": "हटाएँ", "open": "खुला", "open_public_course": "सार्वजनिक कोर्स खोलें", + "view_course_site": "कोर्स साइट देखें", + "publish_course": "कोर्स प्रकाशित करें", "copy_course_url": "कोर्स URL कॉपी करें" }, "due_date": "देय", diff --git a/apps/dashboard/src/lib/utils/translations/pl.json b/apps/dashboard/src/lib/utils/translations/pl.json index 793ca2b4..ebc3ca4c 100644 --- a/apps/dashboard/src/lib/utils/translations/pl.json +++ b/apps/dashboard/src/lib/utils/translations/pl.json @@ -2234,7 +2234,14 @@ "header": { "preview": "Preview course", "preview_missing_slug": "Generate and save a course slug before previewing.", - "view_as_student": "Wyświetl jako student" + "view_as_student": "Wyświetl jako student", + "view_course_site": "Zobacz stronę kursu" + }, + "view_course_site": { + "unpublished_title": "Opublikuj swój kurs", + "unpublished_description": "Powinieneś opublikować ten kurs, zanim zobaczysz stronę", + "cancel": "Anuluj", + "publish_course": "Opublikuj kurs" }, "sidebar": { "back_to_course": "Powrót na kurs", @@ -2420,6 +2427,8 @@ "delete": "Usuń", "open": "Otwórz", "open_public_course": "Otwórz publiczny kurs", + "view_course_site": "Zobacz stronę kursu", + "publish_course": "Opublikuj kurs", "copy_course_url": "Kopiuj adres URL kursu" }, "due_date": "Termin", diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json index f49962d9..955d91c1 100644 --- a/apps/dashboard/src/lib/utils/translations/pt.json +++ b/apps/dashboard/src/lib/utils/translations/pt.json @@ -2234,7 +2234,14 @@ "header": { "preview": "Preview course", "preview_missing_slug": "Generate and save a course slug before previewing.", - "view_as_student": "Ver como aluno" + "view_as_student": "Ver como aluno", + "view_course_site": "Ver site do curso" + }, + "view_course_site": { + "unpublished_title": "Publique seu curso", + "unpublished_description": "Você deve publicar este curso antes de poder ver o site", + "cancel": "Cancelar", + "publish_course": "Publicar curso" }, "sidebar": { "back_to_course": "De volta ao curso", @@ -2420,6 +2427,8 @@ "delete": "Excluir", "open": "Abrir", "open_public_course": "Abrir curso público", + "view_course_site": "Ver site do curso", + "publish_course": "Publicar curso", "copy_course_url": "Copiar URL do curso" }, "due_date": "Devido", diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json index 7e970810..7d4e243c 100644 --- a/apps/dashboard/src/lib/utils/translations/ru.json +++ b/apps/dashboard/src/lib/utils/translations/ru.json @@ -2234,7 +2234,14 @@ "header": { "preview": "Preview course", "preview_missing_slug": "Generate and save a course slug before previewing.", - "view_as_student": "Посмотреть как студент" + "view_as_student": "Посмотреть как студент", + "view_course_site": "Посмотреть сайт курса" + }, + "view_course_site": { + "unpublished_title": "Опубликуйте курс", + "unpublished_description": "Перед просмотром сайта необходимо опубликовать этот курс", + "cancel": "Отмена", + "publish_course": "Опубликовать курс" }, "sidebar": { "back_to_course": "Вернуться к курсу", @@ -2420,6 +2427,8 @@ "delete": "Удалить", "open": "Открыть", "open_public_course": "Открыть публичный курс", + "view_course_site": "Посмотреть сайт курса", + "publish_course": "Опубликовать курс", "copy_course_url": "Копировать URL курса" }, "due_date": "Срок погашения", diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json index 7c2b9846..2fc5401b 100644 --- a/apps/dashboard/src/lib/utils/translations/vi.json +++ b/apps/dashboard/src/lib/utils/translations/vi.json @@ -2234,7 +2234,14 @@ "header": { "preview": "Preview course", "preview_missing_slug": "Generate and save a course slug before previewing.", - "view_as_student": "Xem với tư cách sinh viên" + "view_as_student": "Xem với tư cách sinh viên", + "view_course_site": "Xem trang khóa học" + }, + "view_course_site": { + "unpublished_title": "Xuất bản khóa học của bạn", + "unpublished_description": "Bạn nên xuất bản khóa học này trước khi có thể xem trang web", + "cancel": "Hủy", + "publish_course": "Xuất bản khóa học" }, "sidebar": { "back_to_course": "Quay lại khóa học", @@ -2420,6 +2427,8 @@ "delete": "Xóa", "open": "Mở", "open_public_course": "Mở khóa học công khai", + "view_course_site": "Xem trang khóa học", + "publish_course": "Xuất bản khóa học", "copy_course_url": "Sao chép URL khóa học" }, "due_date": "Đến hạn",