fix: validation of allowing teachers create compliance courses without setting deadline (#778)

* Fixed Validation of allowing teachers create compliance courses without setting deadline

* greptile highlightCertificateSettings decraration suggestion

* greptile multiple error bar showing fix suggestion

* organization race condition

* Delete screenshots/Screenshot 2026-07-22 at 11.52.00 AM.png

* deleted unwanted files

* code review fixes

* Changed AttentionHightlight component to work with just one parameter

created fixed Route Enums, Paths and Sections for

* Kebab-case naming fix

* Changes for code comments

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
Victor Jack
2026-08-05 13:28:32 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 05936e788d
commit 60056a6236
16 changed files with 359 additions and 83 deletions
@@ -1,4 +1,5 @@
<script lang="ts">
import { page } from '$app/state';
import * as UnderlineTabs from '@cio/ui/custom/underline-tabs';
import { t } from '$lib/utils/functions/translations';
@@ -12,7 +13,17 @@
activeTab?: string;
};
let { errors, activeTab = $bindable('design') }: Props = $props();
let { errors }: Props = $props();
let activeTab = $state('design');
// Initialize activeTab from URL parameter if present
$effect(() => {
const tabParam = page.url.searchParams.get('tab');
if (tabParam && (tabParam === 'design' || tabParam === 'settings')) {
activeTab = tabParam;
}
});
</script>
<UpgradeBanner>{$t('upgrade.certificate')}</UpgradeBanner>
@@ -7,7 +7,9 @@
import { courseApi } from '$features/course/api';
import { isFreePlan } from '$lib/utils/store/org';
import { getOrderedNavigableContent } from '$features/course/utils/content';
import type { CourseCertificate } from '$features/course/utils/types';
import { ContentType } from '@cio/utils/constants/content';
import { AttentionHighlight } from '$features/ui';
type Props = {
errors: Record<string, string>;
@@ -70,6 +72,16 @@
function onEmailMessageInput(e: Event) {
updateCertificate({ emailMessage: (e.currentTarget as HTMLTextAreaElement).value || null });
}
function validateCertificateSettings(certificate: CourseCertificate | null | undefined): string[] {
const errors: string[] = [];
if (certificate?.isDownloadable && !certificate?.deadline) {
errors.push('course.certification.deadline_required');
}
return errors;
}
const certificateValidationErrors = $derived(validateCertificateSettings(courseApi.course?.certificate));
</script>
<Field.Group class="w-full max-w-md! px-2">
@@ -85,45 +97,62 @@
}}
disabled={$isFreePlan}
/>
<Field.Label for="certificate-downloadable" class="text-gray-600">
{$t('course.navItem.certificates.allow')}
</Field.Label>
<div class="flex-1">
<Field.Label for="certificate-downloadable" class="text-gray-600">
{$t('course.navItem.certificates.allow')}
</Field.Label>
{#if courseApi.course?.certificate?.isDownloadable && certificateValidationErrors.includes('course.certification.deadline_required')}
<Field.Error class="mt-1">{$t('course.certification.deadline_required')}</Field.Error>
{/if}
</div>
</Field.Field>
<Field.Group>
<Field.Field>
<Field.Label for="cert-deadline">{$t('course.certification.deadline_label')}</Field.Label>
<Input
id="cert-deadline"
type="datetime-local"
class="w-full"
value={isoToDatetimeLocal(courseApi.course?.certificate?.deadline ?? null)}
onchange={onCertDeadlineChange}
disabled={$isFreePlan}
/>
<Field.Description>{$t('course.certification.deadline_helper')}</Field.Description>
{#if errors['certificate.deadline']}
<Field.Error>{errors['certificate.deadline']}</Field.Error>
{/if}
</Field.Field>
<AttentionHighlight id="cert-deadline">
<Field.Field>
<Field.Label for="cert-deadline">
{$t('course.certification.deadline_label')}
{#if courseApi.course?.certificate?.isDownloadable}
<span class="ui:text-red-600">*</span>
{/if}
</Field.Label>
<Input
id="cert-deadline"
type="datetime-local"
class="w-full"
value={isoToDatetimeLocal(courseApi.course?.certificate?.deadline ?? null)}
onchange={onCertDeadlineChange}
disabled={$isFreePlan}
/>
<Field.Description>{$t('course.certification.deadline_helper')}</Field.Description>
{#if errors['certificate.deadline']}
<Field.Error>{errors['certificate.deadline']}</Field.Error>
{/if}
{#if certificateValidationErrors.includes('course.certification.deadline_required')}
<Field.Error>{$t('course.certification.deadline_required')}</Field.Error>
{/if}
</Field.Field>
</AttentionHighlight>
<Field.Field>
<Field.Label for="cert-threshold">{$t('course.certification.threshold_label')}</Field.Label>
<Input
id="cert-threshold"
type="number"
min={0}
max={100}
class="w-full"
value={String(courseApi.course?.certificate?.threshold ?? 100)}
oninput={onThresholdInput}
disabled={$isFreePlan}
/>
<Field.Description>{$t('course.certification.threshold_helper')}</Field.Description>
{#if errors['certificate.threshold']}
<Field.Error>{errors['certificate.threshold']}</Field.Error>
{/if}
</Field.Field>
<AttentionHighlight id="cert-threshold">
<Field.Field>
<Field.Label for="cert-threshold">{$t('course.certification.threshold_label')}</Field.Label>
<Input
id="cert-threshold"
type="number"
min={0}
max={100}
class="w-full"
value={String(courseApi.course?.certificate?.threshold ?? 100)}
oninput={onThresholdInput}
disabled={$isFreePlan}
/>
<Field.Description>{$t('course.certification.threshold_helper')}</Field.Description>
{#if errors['certificate.threshold']}
<Field.Error>{errors['certificate.threshold']}</Field.Error>
{/if}
</Field.Field>
</AttentionHighlight>
<Field.Field>
<Field.Label for="cert-final-exercise">{$t('course.certification.final_exercise_label')}</Field.Label>
@@ -7,6 +7,8 @@
import { copyPublicCoursePageUrl, openCoursePreview } from '$features/course/utils/course-preview';
import { currentOrgDomain } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
import { goAndHighlight } from '$lib/routing/go-and-highlight';
import { ROUTE_NAME, ROUTE_SECTIONS } from '$lib/routing/routes';
interface Props {
id: string;
@@ -65,12 +67,7 @@
}
function handlePublishCourse() {
if (page.url.pathname === courseSettingsPath) {
scrollToSettingsSection('publish');
return;
}
redirect(`/courses/${id}/settings#publish`);
goAndHighlight(ROUTE_NAME.COURSE_SETTINGS, ROUTE_SECTIONS[ROUTE_NAME.COURSE_SETTINGS].PUBLISH, { id });
}
function handleDeleteCourse() {
@@ -418,7 +418,7 @@ export function handleReorderQuestionsInSection(exerciseSectionId: string, order
.map((questionId) => questionById.get(String(questionId)))
.filter(
(question): question is Question =>
Boolean(question) && !question.deletedAt && question.exerciseSectionId === exerciseSectionId
Boolean(question) && !question?.deletedAt && question?.exerciseSectionId === exerciseSectionId
);
const missingSectionQuestions = sectionQuestions.filter(
(question) => !orderedQuestionIdSet.has(String(question.id))