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.
This commit is contained in:
@@ -1,16 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||||
import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
|
|
||||||
import CopyIcon from '@lucide/svelte/icons/copy';
|
|
||||||
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 { IconButton } from '@cio/ui/custom/icon-button';
|
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import CourseContextMenuContent from './course-context-menu-content.svelte';
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import { resolve } from '$app/paths';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -34,43 +26,6 @@
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const showPublicCourseLinks = $derived(isPublished && courseType === 'PUBLIC' && slug.trim().length > 0);
|
const showPublicCourseLinks = $derived(isPublished && courseType === 'PUBLIC' && slug.trim().length > 0);
|
||||||
|
|
||||||
function redirect(url: string) {
|
|
||||||
goto(resolve(url, {}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleShareCourse() {
|
|
||||||
redirect(`/courses/${id}/settings#share`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleInvite() {
|
|
||||||
redirect(`/courses/${id}/people?add=true`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteCourse() {
|
|
||||||
$deleteCourseModal.open = true;
|
|
||||||
$deleteCourseModal.id = id;
|
|
||||||
$deleteCourseModal.title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCloneCourse() {
|
|
||||||
$copyCourseModal.open = true;
|
|
||||||
$copyCourseModal.id = id;
|
|
||||||
$copyCourseModal.title = title;
|
|
||||||
$copyCourseModal.description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOpenPublicCourse() {
|
|
||||||
openCoursePreview({
|
|
||||||
courseId: id,
|
|
||||||
courseSlug: slug,
|
|
||||||
currentOrgDomain: $currentOrgDomain
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCopyCourseUrl() {
|
|
||||||
await copyPublicCoursePageUrl(slug, $currentOrgDomain);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if lmsPublicQuickOnly ? showPublicCourseLinks : true}
|
{#if lmsPublicQuickOnly ? showPublicCourseLinks : true}
|
||||||
@@ -84,41 +39,7 @@
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content align="end">
|
<DropdownMenu.Content align="end">
|
||||||
{#if lmsPublicQuickOnly}
|
<CourseContextMenuContent {id} {title} {description} {isPublished} {courseType} {slug} {lmsPublicQuickOnly} />
|
||||||
<DropdownMenu.Item onclick={handleOpenPublicCourse}>
|
|
||||||
<ExternalLinkIcon size={16} class="mr-2" />
|
|
||||||
{$t('courses.course_card.context_menu.open_public_course')}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item onclick={() => void handleCopyCourseUrl()}>
|
|
||||||
<CopyIcon size={16} class="mr-2" />
|
|
||||||
{$t('courses.course_card.context_menu.copy_course_url')}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
{:else}
|
|
||||||
{#if showPublicCourseLinks}
|
|
||||||
<DropdownMenu.Item onclick={handleOpenPublicCourse}>
|
|
||||||
<ExternalLinkIcon size={16} class="mr-2" />
|
|
||||||
{$t('courses.course_card.context_menu.open_public_course')}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item onclick={() => void handleCopyCourseUrl()}>
|
|
||||||
<CopyIcon size={16} class="mr-2" />
|
|
||||||
{$t('courses.course_card.context_menu.copy_course_url')}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
{/if}
|
|
||||||
<DropdownMenu.Item onclick={handleCloneCourse}>
|
|
||||||
{$t('courses.course_card.context_menu.clone')}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item onclick={handleShareCourse}>
|
|
||||||
{$t('courses.course_card.context_menu.share')}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item onclick={handleInvite}>
|
|
||||||
{$t('courses.course_card.context_menu.invite')}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.Item class="text-red-600" onclick={handleDeleteCourse}>
|
|
||||||
{$t('courses.course_card.context_menu.delete')}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
{/if}
|
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||||
|
import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
|
||||||
|
import CopyIcon from '@lucide/svelte/icons/copy';
|
||||||
|
import EyeIcon from '@lucide/svelte/icons/eye';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
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 { t } from '$lib/utils/functions/translations';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
isPublished?: boolean;
|
||||||
|
courseType?: string | null;
|
||||||
|
slug?: string;
|
||||||
|
/** Compact menu for LMS course cards */
|
||||||
|
lmsPublicQuickOnly?: boolean;
|
||||||
|
/** Include "View as student" (course header menu) */
|
||||||
|
includeViewAsStudent?: boolean;
|
||||||
|
onViewAsStudent?: () => void;
|
||||||
|
/** List view includes an explicit Open action */
|
||||||
|
includeOpen?: boolean;
|
||||||
|
/** Hide org management actions (clone, share, invite, delete) */
|
||||||
|
hideOrgActions?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
isPublished = false,
|
||||||
|
courseType = null,
|
||||||
|
slug = '',
|
||||||
|
lmsPublicQuickOnly = false,
|
||||||
|
includeViewAsStudent = false,
|
||||||
|
onViewAsStudent,
|
||||||
|
includeOpen = false,
|
||||||
|
hideOrgActions = false
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
const showPublicCourseLinks = $derived(isPublished && courseType === 'PUBLIC' && slug.trim().length > 0);
|
||||||
|
|
||||||
|
function redirect(url: string) {
|
||||||
|
goto(resolve(url, {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleShareCourse() {
|
||||||
|
redirect(`/courses/${id}/settings#share`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInvite() {
|
||||||
|
redirect(`/courses/${id}/people?add=true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePublishCourse() {
|
||||||
|
redirect(`/courses/${id}/settings#publish`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteCourse() {
|
||||||
|
$deleteCourseModal.open = true;
|
||||||
|
$deleteCourseModal.id = id;
|
||||||
|
$deleteCourseModal.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCloneCourse() {
|
||||||
|
$copyCourseModal.open = true;
|
||||||
|
$copyCourseModal.id = id;
|
||||||
|
$copyCourseModal.title = title;
|
||||||
|
$copyCourseModal.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenCourse() {
|
||||||
|
redirect(`/courses/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleViewCourseSite() {
|
||||||
|
openCoursePreview({
|
||||||
|
courseId: id,
|
||||||
|
courseSlug: slug,
|
||||||
|
currentOrgDomain: $currentOrgDomain
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCopyCourseUrl() {
|
||||||
|
await copyPublicCoursePageUrl(slug, $currentOrgDomain);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if lmsPublicQuickOnly}
|
||||||
|
<DropdownMenu.Item onclick={handleViewCourseSite}>
|
||||||
|
<ExternalLinkIcon size={16} class="mr-2" />
|
||||||
|
{$t('courses.course_card.context_menu.view_course_site')}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onclick={() => void handleCopyCourseUrl()}>
|
||||||
|
<CopyIcon size={16} class="mr-2" />
|
||||||
|
{$t('courses.course_card.context_menu.copy_course_url')}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{:else}
|
||||||
|
{#if includeViewAsStudent}
|
||||||
|
<DropdownMenu.Item onclick={() => onViewAsStudent?.()}>
|
||||||
|
<EyeIcon size={16} class="mr-2" />
|
||||||
|
{$t('course.header.view_as_student')}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if isPublished}
|
||||||
|
<DropdownMenu.Item onclick={handleViewCourseSite}>
|
||||||
|
<ExternalLinkIcon size={16} class="mr-2" />
|
||||||
|
{$t('courses.course_card.context_menu.view_course_site')}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{#if showPublicCourseLinks}
|
||||||
|
<DropdownMenu.Item onclick={() => void handleCopyCourseUrl()}>
|
||||||
|
<CopyIcon size={16} class="mr-2" />
|
||||||
|
{$t('courses.course_card.context_menu.copy_course_url')}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/if}
|
||||||
|
{#if !hideOrgActions || includeOpen}
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<DropdownMenu.Item onclick={handlePublishCourse}>
|
||||||
|
{$t('courses.course_card.context_menu.publish_course')}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{#if !hideOrgActions || includeOpen}
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if includeOpen}
|
||||||
|
<DropdownMenu.Item onclick={handleOpenCourse}>
|
||||||
|
{$t('courses.course_card.context_menu.open')}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !hideOrgActions}
|
||||||
|
<DropdownMenu.Item onclick={handleCloneCourse}>
|
||||||
|
{$t('courses.course_card.context_menu.clone')}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onclick={handleShareCourse}>
|
||||||
|
{$t('courses.course_card.context_menu.share')}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onclick={handleInvite}>
|
||||||
|
{$t('courses.course_card.context_menu.invite')}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item class="text-red-600" onclick={handleDeleteCourse}>
|
||||||
|
{$t('courses.course_card.context_menu.delete')}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Separator } from '@cio/ui/base/separator';
|
import { Separator } from '@cio/ui/base/separator';
|
||||||
import * as Sidebar from '@cio/ui/base/sidebar';
|
import * as Sidebar from '@cio/ui/base/sidebar';
|
||||||
import EyeIcon from '@lucide/svelte/icons/eye';
|
import * as ButtonGroup from '@cio/ui/base/button-group';
|
||||||
|
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||||
|
import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
|
||||||
|
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||||
import { Button } from '@cio/ui/base/button';
|
import { Button } from '@cio/ui/base/button';
|
||||||
import { Waves } from '@cio/ui/custom/animation';
|
import { Waves } from '@cio/ui/custom/animation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
@@ -12,23 +15,46 @@
|
|||||||
import { courseApi } from '$features/course/api';
|
import { courseApi } from '$features/course/api';
|
||||||
import { getActiveCourseNavKey } from '$features/course/utils/functions';
|
import { getActiveCourseNavKey } from '$features/course/utils/functions';
|
||||||
import { toggleAiAssistant } from '$features/ai-assistant/utils/store';
|
import { toggleAiAssistant } from '$features/ai-assistant/utils/store';
|
||||||
|
import { openCoursePreview } from '$features/course/utils/course-preview';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
import CoursePublishBadge from './course-publish-badge.svelte';
|
import CoursePublishBadge from './course-publish-badge.svelte';
|
||||||
import CoursePublicBadge from './course-public-badge.svelte';
|
import CoursePublicBadge from './course-public-badge.svelte';
|
||||||
|
import CourseContextMenuContent from './course-context-menu-content.svelte';
|
||||||
import ViewAsStudentModal from './view-as-student-modal.svelte';
|
import ViewAsStudentModal from './view-as-student-modal.svelte';
|
||||||
|
import ViewCourseSiteUnpublishedModal from './view-course-site-unpublished-modal.svelte';
|
||||||
|
|
||||||
const siteName = $derived($currentOrg.siteName);
|
const siteName = $derived($currentOrg.siteName);
|
||||||
const showCoursePublishBadge = $derived(!$isStudentExperience);
|
const showCoursePublishBadge = $derived(!$isStudentExperience);
|
||||||
const isPublicCourse = $derived(courseApi.course?.type === 'PUBLIC');
|
const isPublicCourse = $derived(courseApi.course?.type === 'PUBLIC');
|
||||||
const activeNavKey = $derived(getActiveCourseNavKey(page.url.pathname, courseApi.course?.id ?? ''));
|
const activeNavKey = $derived(getActiveCourseNavKey(page.url.pathname, courseApi.course?.id ?? ''));
|
||||||
|
const isPublished = $derived(courseApi.course?.isPublished ?? false);
|
||||||
|
|
||||||
let viewAsStudentOpen = $state(false);
|
let viewAsStudentOpen = $state(false);
|
||||||
|
let viewCourseSiteUnpublishedOpen = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!siteName) return;
|
if (!siteName) return;
|
||||||
|
|
||||||
setupProgressApi.fetchSetupProgress(siteName);
|
setupProgressApi.fetchSetupProgress(siteName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleViewCourseSite() {
|
||||||
|
const course = courseApi.course;
|
||||||
|
if (!course?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPublished) {
|
||||||
|
viewCourseSiteUnpublishedOpen = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openCoursePreview({
|
||||||
|
courseId: course.id,
|
||||||
|
courseSlug: course.slug,
|
||||||
|
currentOrgDomain: $currentOrgDomain
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
@@ -52,7 +78,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showCoursePublishBadge}
|
{#if showCoursePublishBadge}
|
||||||
<CoursePublishBadge isPublished={courseApi.course?.isPublished ?? false} />
|
<CoursePublishBadge {isPublished} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,16 +104,47 @@
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{#if !$isStudentExperience}
|
{#if !$isStudentExperience}
|
||||||
|
<ButtonGroup.Root>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onclick={() => (viewAsStudentOpen = true)}
|
onclick={handleViewCourseSite}
|
||||||
disabled={!courseApi.course?.id}
|
disabled={!courseApi.course?.id}
|
||||||
aria-label={$t('course.header.view_as_student')}
|
aria-label={$t('course.header.view_course_site')}
|
||||||
>
|
>
|
||||||
<EyeIcon size={14} />
|
<ExternalLinkIcon size={14} />
|
||||||
<span class="hidden sm:inline">{$t('course.header.view_as_student')}</span>
|
<span class="hidden sm:inline">{$t('course.header.view_course_site')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
aria-label={$t('courses.course_card.actions_menu_aria')}
|
||||||
|
disabled={!courseApi.course?.id}
|
||||||
|
>
|
||||||
|
<EllipsisVerticalIcon size={14} />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content align="end">
|
||||||
|
{#if courseApi.course}
|
||||||
|
<CourseContextMenuContent
|
||||||
|
id={courseApi.course.id}
|
||||||
|
title={courseApi.course.title}
|
||||||
|
description={courseApi.course.description}
|
||||||
|
isPublished={courseApi.course.isPublished ?? false}
|
||||||
|
courseType={courseApi.course.type}
|
||||||
|
slug={courseApi.course.slug ?? ''}
|
||||||
|
includeViewAsStudent={true}
|
||||||
|
onViewAsStudent={() => (viewAsStudentOpen = true)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</ButtonGroup.Root>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -98,3 +155,5 @@
|
|||||||
courseSlug={courseApi.course?.slug}
|
courseSlug={courseApi.course?.slug}
|
||||||
currentOrgDomain={$currentOrgDomain}
|
currentOrgDomain={$currentOrgDomain}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ViewCourseSiteUnpublishedModal bind:open={viewCourseSiteUnpublishedOpen} currentOrgDomain={$currentOrgDomain} />
|
||||||
|
|||||||
@@ -10,14 +10,10 @@
|
|||||||
import { ContentType } from '@cio/utils/constants/content';
|
import { ContentType } from '@cio/utils/constants/content';
|
||||||
import { Image } from '$features/ui';
|
import { Image } from '$features/ui';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
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 { buildCoursePlaceholderAvatarUrl } from '$features/course/utils/course-list-row-utils';
|
||||||
import CourseContentIcon from './course-content-icon.svelte';
|
import CourseContentIcon from './course-content-icon.svelte';
|
||||||
import CoursePublicBadge from './course-public-badge.svelte';
|
import CoursePublicBadge from './course-public-badge.svelte';
|
||||||
import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
|
import CourseContextMenuContent from './course-context-menu-content.svelte';
|
||||||
import CopyIcon from '@lucide/svelte/icons/copy';
|
|
||||||
|
|
||||||
interface Tag {
|
interface Tag {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -133,30 +129,6 @@
|
|||||||
}
|
}
|
||||||
goto(resolve(`/courses/${id}`, {}));
|
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);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ResourceListRow.Root
|
<ResourceListRow.Root
|
||||||
@@ -317,37 +289,27 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content align="end">
|
<DropdownMenu.Content align="end">
|
||||||
{#if showPublicCourseLinks}
|
{#if isLMS}
|
||||||
<DropdownMenu.Item
|
<CourseContextMenuContent
|
||||||
onclick={() =>
|
{id}
|
||||||
openCoursePreview({
|
{title}
|
||||||
courseId: id,
|
{description}
|
||||||
courseSlug: slug,
|
{isPublished}
|
||||||
currentOrgDomain: $currentOrgDomain
|
courseType={type}
|
||||||
})}
|
{slug}
|
||||||
>
|
lmsPublicQuickOnly={true}
|
||||||
<ExternalLinkIcon class="mr-2 size-4" />
|
/>
|
||||||
{$t('courses.course_card.context_menu.open_public_course')}
|
{:else}
|
||||||
</DropdownMenu.Item>
|
<CourseContextMenuContent
|
||||||
<DropdownMenu.Item onclick={() => void handleCopyCourseUrl()}>
|
{id}
|
||||||
<CopyIcon class="mr-2 size-4" />
|
{title}
|
||||||
{$t('courses.course_card.context_menu.copy_course_url')}
|
{description}
|
||||||
</DropdownMenu.Item>
|
{isPublished}
|
||||||
{/if}
|
courseType={type}
|
||||||
{#if !isLMS}
|
{slug}
|
||||||
{#if showPublicCourseLinks}
|
includeOpen={true}
|
||||||
<DropdownMenu.Separator />
|
hideOrgActions={false}
|
||||||
{/if}
|
/>
|
||||||
<DropdownMenu.Item onclick={handleOpen}>
|
|
||||||
{$t('courses.course_card.context_menu.open')}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item onclick={handleClone}>
|
|
||||||
{$t('courses.course_card.context_menu.clone')}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.Item variant="destructive" onclick={handleDelete}>
|
|
||||||
{$t('courses.course_card.context_menu.delete')}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
{/if}
|
{/if}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
|||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '@cio/ui/base/button';
|
||||||
|
import * as Dialog from '@cio/ui/base/dialog';
|
||||||
|
import { courseApi } from '$features/course/api';
|
||||||
|
import { publishCourse } from '$features/course/utils/publish-course';
|
||||||
|
import { openCoursePreview } from '$features/course/utils/course-preview';
|
||||||
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open?: boolean;
|
||||||
|
currentOrgDomain?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { open = $bindable(false), currentOrgDomain = '' }: Props = $props();
|
||||||
|
|
||||||
|
let isPublishing = $state(false);
|
||||||
|
|
||||||
|
async function handlePublishCourse() {
|
||||||
|
const course = courseApi.course;
|
||||||
|
if (!course?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPublishing = true;
|
||||||
|
|
||||||
|
const published = await publishCourse(course);
|
||||||
|
isPublishing = false;
|
||||||
|
|
||||||
|
if (!published) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
open = false;
|
||||||
|
|
||||||
|
openCoursePreview({
|
||||||
|
courseId: course.id,
|
||||||
|
courseSlug: courseApi.course?.slug,
|
||||||
|
currentOrgDomain
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog.Root
|
||||||
|
bind:open
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (!isOpen && !isPublishing) {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog.Content class="w-[calc(100%-2rem)] max-w-md! p-4">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>{$t('course.view_course_site.unpublished_title')}</Dialog.Title>
|
||||||
|
<Dialog.Description>{$t('course.view_course_site.unpublished_description')}</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button variant="outline" onclick={() => (open = false)} disabled={isPublishing}>
|
||||||
|
{$t('course.view_course_site.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="default" onclick={handlePublishCourse} loading={isPublishing} disabled={!courseApi.course?.id}>
|
||||||
|
{$t('course.view_course_site.publish_course')}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
@@ -794,7 +794,7 @@
|
|||||||
|
|
||||||
<Field.Separator />
|
<Field.Separator />
|
||||||
|
|
||||||
<Field.Set>
|
<Field.Set id="publish">
|
||||||
<Field.Legend>{$t('course.navItem.settings.publish')}</Field.Legend>
|
<Field.Legend>{$t('course.navItem.settings.publish')}</Field.Legend>
|
||||||
<Field.Description>{$t('course.navItem.settings.determines')}</Field.Description>
|
<Field.Description>{$t('course.navItem.settings.determines')}</Field.Description>
|
||||||
<Field.Field orientation="horizontal">
|
<Field.Field orientation="horizontal">
|
||||||
|
|||||||
@@ -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<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -2234,7 +2234,14 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"preview": "Preview course",
|
"preview": "Preview course",
|
||||||
"preview_missing_slug": "Generate and save a course slug before previewing.",
|
"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": {
|
"sidebar": {
|
||||||
"back_to_course": "Tilbage til kurset",
|
"back_to_course": "Tilbage til kurset",
|
||||||
@@ -2420,6 +2427,8 @@
|
|||||||
"delete": "Slet",
|
"delete": "Slet",
|
||||||
"open": "Åbn",
|
"open": "Åbn",
|
||||||
"open_public_course": "Åbn offentligt kursus",
|
"open_public_course": "Åbn offentligt kursus",
|
||||||
|
"view_course_site": "Se kursusside",
|
||||||
|
"publish_course": "Udgiv kursus",
|
||||||
"copy_course_url": "Kopiér kursus-URL"
|
"copy_course_url": "Kopiér kursus-URL"
|
||||||
},
|
},
|
||||||
"due_date": "Forfalder",
|
"due_date": "Forfalder",
|
||||||
|
|||||||
@@ -2234,7 +2234,14 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"preview": "Preview course",
|
"preview": "Preview course",
|
||||||
"preview_missing_slug": "Generate and save a course slug before previewing.",
|
"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": {
|
"sidebar": {
|
||||||
"back_to_course": "Zurück zum Kurs",
|
"back_to_course": "Zurück zum Kurs",
|
||||||
@@ -2420,6 +2427,8 @@
|
|||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"open": "Offen",
|
"open": "Offen",
|
||||||
"open_public_course": "Öffentlichen Kurs öffnen",
|
"open_public_course": "Öffentlichen Kurs öffnen",
|
||||||
|
"view_course_site": "Kursseite anzeigen",
|
||||||
|
"publish_course": "Kurs veröffentlichen",
|
||||||
"copy_course_url": "Kurs-URL kopieren"
|
"copy_course_url": "Kurs-URL kopieren"
|
||||||
},
|
},
|
||||||
"due_date": "Fällig",
|
"due_date": "Fällig",
|
||||||
|
|||||||
@@ -1970,6 +1970,8 @@
|
|||||||
"invite": "Invite",
|
"invite": "Invite",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"open_public_course": "Open public course",
|
"open_public_course": "Open public course",
|
||||||
|
"view_course_site": "View course site",
|
||||||
|
"publish_course": "Publish course",
|
||||||
"copy_course_url": "Copy course URL"
|
"copy_course_url": "Copy course URL"
|
||||||
},
|
},
|
||||||
"error_message": "An error occurred.",
|
"error_message": "An error occurred.",
|
||||||
@@ -2147,7 +2149,14 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"preview": "Preview course",
|
"preview": "Preview course",
|
||||||
"preview_missing_slug": "Generate and save a course slug before previewing.",
|
"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": {
|
"view_as_student": {
|
||||||
"title": "This will redirect you to the Student LMS.",
|
"title": "This will redirect you to the Student LMS.",
|
||||||
|
|||||||
@@ -2234,7 +2234,14 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"preview": "Preview course",
|
"preview": "Preview course",
|
||||||
"preview_missing_slug": "Generate and save a course slug before previewing.",
|
"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": {
|
"sidebar": {
|
||||||
"back_to_course": "volver al curso",
|
"back_to_course": "volver al curso",
|
||||||
@@ -2420,6 +2427,8 @@
|
|||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
"open": "Abierto",
|
"open": "Abierto",
|
||||||
"open_public_course": "Abrir curso público",
|
"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"
|
"copy_course_url": "Copiar URL del curso"
|
||||||
},
|
},
|
||||||
"due_date": "debido",
|
"due_date": "debido",
|
||||||
|
|||||||
@@ -2234,7 +2234,14 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"preview": "Preview course",
|
"preview": "Preview course",
|
||||||
"preview_missing_slug": "Generate and save a course slug before previewing.",
|
"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": {
|
"sidebar": {
|
||||||
"back_to_course": "Retour au cours",
|
"back_to_course": "Retour au cours",
|
||||||
@@ -2420,6 +2427,8 @@
|
|||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"open": "Ouvert",
|
"open": "Ouvert",
|
||||||
"open_public_course": "Ouvrir le cours public",
|
"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"
|
"copy_course_url": "Copier l’URL du cours"
|
||||||
},
|
},
|
||||||
"due_date": "Due",
|
"due_date": "Due",
|
||||||
|
|||||||
@@ -2234,7 +2234,14 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"preview": "Preview course",
|
"preview": "Preview course",
|
||||||
"preview_missing_slug": "Generate and save a course slug before previewing.",
|
"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": {
|
"sidebar": {
|
||||||
"back_to_course": "पाठ्यक्रम पर वापस जाएँ",
|
"back_to_course": "पाठ्यक्रम पर वापस जाएँ",
|
||||||
@@ -2420,6 +2427,8 @@
|
|||||||
"delete": "हटाएँ",
|
"delete": "हटाएँ",
|
||||||
"open": "खुला",
|
"open": "खुला",
|
||||||
"open_public_course": "सार्वजनिक कोर्स खोलें",
|
"open_public_course": "सार्वजनिक कोर्स खोलें",
|
||||||
|
"view_course_site": "कोर्स साइट देखें",
|
||||||
|
"publish_course": "कोर्स प्रकाशित करें",
|
||||||
"copy_course_url": "कोर्स URL कॉपी करें"
|
"copy_course_url": "कोर्स URL कॉपी करें"
|
||||||
},
|
},
|
||||||
"due_date": "देय",
|
"due_date": "देय",
|
||||||
|
|||||||
@@ -2234,7 +2234,14 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"preview": "Preview course",
|
"preview": "Preview course",
|
||||||
"preview_missing_slug": "Generate and save a course slug before previewing.",
|
"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": {
|
"sidebar": {
|
||||||
"back_to_course": "Powrót na kurs",
|
"back_to_course": "Powrót na kurs",
|
||||||
@@ -2420,6 +2427,8 @@
|
|||||||
"delete": "Usuń",
|
"delete": "Usuń",
|
||||||
"open": "Otwórz",
|
"open": "Otwórz",
|
||||||
"open_public_course": "Otwórz publiczny kurs",
|
"open_public_course": "Otwórz publiczny kurs",
|
||||||
|
"view_course_site": "Zobacz stronę kursu",
|
||||||
|
"publish_course": "Opublikuj kurs",
|
||||||
"copy_course_url": "Kopiuj adres URL kursu"
|
"copy_course_url": "Kopiuj adres URL kursu"
|
||||||
},
|
},
|
||||||
"due_date": "Termin",
|
"due_date": "Termin",
|
||||||
|
|||||||
@@ -2234,7 +2234,14 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"preview": "Preview course",
|
"preview": "Preview course",
|
||||||
"preview_missing_slug": "Generate and save a course slug before previewing.",
|
"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": {
|
"sidebar": {
|
||||||
"back_to_course": "De volta ao curso",
|
"back_to_course": "De volta ao curso",
|
||||||
@@ -2420,6 +2427,8 @@
|
|||||||
"delete": "Excluir",
|
"delete": "Excluir",
|
||||||
"open": "Abrir",
|
"open": "Abrir",
|
||||||
"open_public_course": "Abrir curso público",
|
"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"
|
"copy_course_url": "Copiar URL do curso"
|
||||||
},
|
},
|
||||||
"due_date": "Devido",
|
"due_date": "Devido",
|
||||||
|
|||||||
@@ -2234,7 +2234,14 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"preview": "Preview course",
|
"preview": "Preview course",
|
||||||
"preview_missing_slug": "Generate and save a course slug before previewing.",
|
"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": {
|
"sidebar": {
|
||||||
"back_to_course": "Вернуться к курсу",
|
"back_to_course": "Вернуться к курсу",
|
||||||
@@ -2420,6 +2427,8 @@
|
|||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"open": "Открыть",
|
"open": "Открыть",
|
||||||
"open_public_course": "Открыть публичный курс",
|
"open_public_course": "Открыть публичный курс",
|
||||||
|
"view_course_site": "Посмотреть сайт курса",
|
||||||
|
"publish_course": "Опубликовать курс",
|
||||||
"copy_course_url": "Копировать URL курса"
|
"copy_course_url": "Копировать URL курса"
|
||||||
},
|
},
|
||||||
"due_date": "Срок погашения",
|
"due_date": "Срок погашения",
|
||||||
|
|||||||
@@ -2234,7 +2234,14 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"preview": "Preview course",
|
"preview": "Preview course",
|
||||||
"preview_missing_slug": "Generate and save a course slug before previewing.",
|
"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": {
|
"sidebar": {
|
||||||
"back_to_course": "Quay lại khóa học",
|
"back_to_course": "Quay lại khóa học",
|
||||||
@@ -2420,6 +2427,8 @@
|
|||||||
"delete": "Xóa",
|
"delete": "Xóa",
|
||||||
"open": "Mở",
|
"open": "Mở",
|
||||||
"open_public_course": "Mở khóa học công khai",
|
"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"
|
"copy_course_url": "Sao chép URL khóa học"
|
||||||
},
|
},
|
||||||
"due_date": "Đến hạn",
|
"due_date": "Đến hạn",
|
||||||
|
|||||||
Reference in New Issue
Block a user