mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
added new requirements and fix UI bugs
This commit is contained in:
@@ -6,10 +6,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
ArrowLeft, ChevronLeft, ChevronRight, Check,
|
||||
FileText, LayoutTemplate, ClipboardCheck,
|
||||
FileText, LayoutTemplate, ClipboardCheck, ListChecks,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useLibrary } from "@/contexts/AdminLibraryContext";
|
||||
import { useCourses } from "@/contexts/AdminCoursesContext";
|
||||
import DraftRequirementsEditor, { DraftRequirementsSummary } from "@/modules/admin/components/courses/DraftRequirementsEditor";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { PageMeta } from "@/contexts/MetadataContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -42,7 +44,8 @@ const DEFAULT_VALUES = { title: "", description: "", blocks: [] };
|
||||
const STEPS = [
|
||||
{ id: 0, label: "Lesson", icon: FileText },
|
||||
{ id: 1, label: "Page Builder", icon: LayoutTemplate },
|
||||
{ id: 2, label: "Review", icon: ClipboardCheck },
|
||||
{ id: 2, label: "Requirements", icon: ListChecks },
|
||||
{ id: 3, label: "Review", icon: ClipboardCheck },
|
||||
];
|
||||
|
||||
// Fields validated with trigger() before advancing past each step.
|
||||
@@ -157,7 +160,19 @@ function StepPageBuilder({ control, setValue, getValues }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Step 3 — Review ─────────────────────────────────────────────────────────────
|
||||
// ─── Step 3 — Requirements ───────────────────────────────────────────────────────
|
||||
function StepRequirements({ requirements, setRequirements, blockTypes }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure how learners complete this lesson — optional, sensible defaults apply automatically. This is created together with the rest of the lesson when you finish.
|
||||
</p>
|
||||
<DraftRequirementsEditor entityType="lesson" items={requirements} onChange={setRequirements} blockTypes={blockTypes} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Step 4 — Review ─────────────────────────────────────────────────────────────
|
||||
function SummaryRow({ label, value }) {
|
||||
if (!value) return null;
|
||||
return (
|
||||
@@ -168,7 +183,7 @@ function SummaryRow({ label, value }) {
|
||||
);
|
||||
}
|
||||
|
||||
function StepReview({ data, attachUnitId }) {
|
||||
function StepReview({ data, attachUnitId, requirements }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="border border-border rounded-lg p-4 space-y-1">
|
||||
@@ -181,6 +196,8 @@ function StepReview({ data, attachUnitId }) {
|
||||
<SummaryRow label="Page content" value={`${(data.blocks ?? []).length} block(s)`} />
|
||||
{attachUnitId && <SummaryRow label="Attaches to" value="The unit you came from" />}
|
||||
</div>
|
||||
|
||||
<DraftRequirementsSummary entityType="lesson" items={requirements} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -190,12 +207,14 @@ export default function AddLibraryLesson() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { createLesson, saveLessonPage, loading } = useLibrary();
|
||||
const { syncLessonRequirements } = useCourses();
|
||||
const { user } = useAuth();
|
||||
|
||||
// ?unit_id=… → create-and-attach in one call (from the unit lessons manager)
|
||||
const attachUnitId = searchParams.get("unit_id");
|
||||
|
||||
const [step, setStep] = useState(0);
|
||||
const [requirements, setRequirements] = useState([]);
|
||||
|
||||
const {
|
||||
register, control, trigger, getValues, setValue,
|
||||
@@ -206,7 +225,7 @@ export default function AddLibraryLesson() {
|
||||
mode: "onTouched",
|
||||
});
|
||||
|
||||
const { bypassOnce, dialog: unsavedChangesDialog } = useUnsavedChangesGuard(isDirty);
|
||||
const { bypassOnce, dialog: unsavedChangesDialog } = useUnsavedChangesGuard(isDirty || requirements.length > 0);
|
||||
|
||||
const handleNext = async () => {
|
||||
const fields = STEP_FIELDS[step];
|
||||
@@ -219,8 +238,9 @@ export default function AddLibraryLesson() {
|
||||
else setStep((s) => s - 1);
|
||||
};
|
||||
|
||||
// Called manually on click — no <form> tag, so no accidental submit from
|
||||
// Enter or another button while stepping through the wizard.
|
||||
// The one and only persistence point — nothing is created until this final
|
||||
// click at Review, so Requirements (the step before it) is purely a draft
|
||||
// form. No <form> tag wraps the wizard, so this is invoked manually.
|
||||
const handleCreate = async () => {
|
||||
const valid = await trigger();
|
||||
if (!valid) return;
|
||||
@@ -235,10 +255,17 @@ export default function AddLibraryLesson() {
|
||||
if (!result) return;
|
||||
|
||||
const lessonId = result?.data?.data?.lesson_id;
|
||||
if (lessonId && (data.blocks ?? []).length > 0) {
|
||||
if (!lessonId) return;
|
||||
|
||||
if ((data.blocks ?? []).length > 0) {
|
||||
await saveLessonPage(lessonId, { blocks: data.blocks, updatedBy: user?.user_id });
|
||||
}
|
||||
|
||||
if (requirements.length > 0) {
|
||||
const clean = requirements.map(({ _key, ...r }) => r);
|
||||
await syncLessonRequirements(null, null, lessonId, clean);
|
||||
}
|
||||
|
||||
bypassOnce();
|
||||
navigate(attachUnitId ? `/admin/units/${attachUnitId}/view` : "/admin/lessons");
|
||||
};
|
||||
@@ -311,7 +338,14 @@ export default function AddLibraryLesson() {
|
||||
<StepPageBuilder control={control} setValue={setValue} getValues={getValues} />
|
||||
)}
|
||||
{step === 2 && (
|
||||
<StepReview data={getValues()} attachUnitId={attachUnitId} />
|
||||
<StepRequirements
|
||||
requirements={requirements}
|
||||
setRequirements={setRequirements}
|
||||
blockTypes={(getValues("blocks") ?? []).map((b) => b.type)}
|
||||
/>
|
||||
)}
|
||||
{step === 3 && (
|
||||
<StepReview data={getValues()} attachUnitId={attachUnitId} requirements={requirements} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -328,6 +362,9 @@ export default function AddLibraryLesson() {
|
||||
<ChevronRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
) : (
|
||||
// Only the true final step (Review) actually persists anything —
|
||||
// the lesson, its page content, and any draft requirements are
|
||||
// all created together in one shot here.
|
||||
<Button type="button" onClick={handleCreate} disabled={loading}>
|
||||
{loading && <Spinner className="h-4 w-4 mr-2" />}
|
||||
Create Lesson
|
||||
|
||||
@@ -6,6 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
import { useLibrary } from "@/contexts/AdminLibraryContext";
|
||||
import { useCourses } from "@/contexts/AdminCoursesContext";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { PageMeta } from "@/contexts/MetadataContext";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -14,6 +15,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useUnsavedChangesGuard } from "@/hooks/useUnsavedChangesGuard";
|
||||
import CompletionRequirementBuilder from "@/modules/admin/components/courses/CompletionRequirementBuilder";
|
||||
|
||||
const schema = z.object({
|
||||
title: z.string().min(1, "Title is required."),
|
||||
@@ -29,6 +31,7 @@ export default function EditLibraryLesson() {
|
||||
const navigate = useNavigate();
|
||||
const { lessonId } = useParams();
|
||||
const { fetchLesson, updateLesson, lesson, loading } = useLibrary();
|
||||
const { fetchLessonRequirements, syncLessonRequirements } = useCourses();
|
||||
const { user } = useAuth();
|
||||
|
||||
const { register, handleSubmit, reset, formState: { errors, isDirty } } = useForm({
|
||||
@@ -99,6 +102,19 @@ export default function EditLibraryLesson() {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="rounded-lg border bg-card p-6 space-y-4 mt-6">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold">Completion Requirements</h2>
|
||||
<p className="text-xs text-muted-foreground">What a learner must do for this lesson to count as complete, wherever it's attached.</p>
|
||||
</div>
|
||||
<CompletionRequirementBuilder
|
||||
entityType="lesson"
|
||||
fetchFn={fetchLessonRequirements}
|
||||
syncFn={syncLessonRequirements}
|
||||
args={[null, null, lessonId]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams, Link } from "react-router-dom";
|
||||
import {
|
||||
House, Pencil, LayoutTemplate, FileText, Clock, BookCheck,
|
||||
House, Pencil, LayoutTemplate, Clock, BookCheck,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useLibrary } from "@/contexts/AdminLibraryContext";
|
||||
@@ -11,6 +11,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { formatDuration } from "@/utils/timestamp.util";
|
||||
import { PreviewChrome, PreviewContent } from "../../../components/courses/LessonsPreview";
|
||||
|
||||
export default function ViewLibraryLesson() {
|
||||
const navigate = useNavigate();
|
||||
@@ -67,9 +68,6 @@ export default function ViewLibraryLesson() {
|
||||
<Button size="sm" variant="outline" onClick={() => navigate(`/admin/lessons/${lessonId}/edit`)}>
|
||||
<Pencil className="h-3.5 w-3.5 mr-1.5" /> Edit
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => navigate(`/admin/lessons/${lessonId}/page/view`)}>
|
||||
<FileText className="h-3.5 w-3.5 mr-1.5" /> View Page
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => navigate(`/admin/lessons/${lessonId}/page`)}>
|
||||
<LayoutTemplate className="h-3.5 w-3.5 mr-1.5" /> Page Builder
|
||||
</Button>
|
||||
@@ -127,6 +125,33 @@ export default function ViewLibraryLesson() {
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Content preview ── */}
|
||||
<div className="space-y-2">
|
||||
<h2 className="font-semibold px-1">Content</h2>
|
||||
<PreviewChrome title={lesson?.title}>
|
||||
<div className="p-3 sm:p-5 min-h-[200px]">
|
||||
<PreviewContent
|
||||
lesson={lesson}
|
||||
blocks={blocks}
|
||||
showHeader={false}
|
||||
empty="No content blocks yet."
|
||||
/>
|
||||
{blocks.length === 0 && (
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/admin/lessons/${lessonId}/page`)}
|
||||
>
|
||||
Go to Page Builder
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PreviewChrome>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -6,11 +6,13 @@ import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
ArrowLeft, ChevronLeft, ChevronRight, Check,
|
||||
FileText, BookOpen, LayoutTemplate, ClipboardCheck,
|
||||
FileText, BookOpen, LayoutTemplate, ClipboardCheck, ListChecks,
|
||||
Plus, Trash2,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useLibrary } from "@/contexts/AdminLibraryContext";
|
||||
import { useCourses } from "@/contexts/AdminCoursesContext";
|
||||
import DraftRequirementsEditor, { DraftRequirementsSummary } from "@/modules/admin/components/courses/DraftRequirementsEditor";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { PageMeta } from "@/contexts/MetadataContext";
|
||||
import api from "@/utils/api.util";
|
||||
@@ -46,6 +48,7 @@ const lessonSchema = z.object({
|
||||
z.object({ value: z.string().min(1, "Objective cannot be empty.") })
|
||||
).optional(),
|
||||
blocks: z.array(z.any()).optional(),
|
||||
requirements: z.array(z.any()).optional(),
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
@@ -66,7 +69,8 @@ const STEPS = [
|
||||
{ id: 0, label: "Create Unit", icon: FileText },
|
||||
{ id: 1, label: "Lessons", icon: BookOpen },
|
||||
{ id: 2, label: "Page Builder", icon: LayoutTemplate },
|
||||
{ id: 3, label: "Review", icon: ClipboardCheck },
|
||||
{ id: 3, label: "Requirements", icon: ListChecks },
|
||||
{ id: 4, label: "Review", icon: ClipboardCheck },
|
||||
];
|
||||
|
||||
// Fields validated with trigger() before advancing past each step.
|
||||
@@ -194,7 +198,7 @@ function StepLessons({ control, register, errors }) {
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => append({ title: "", description: "", objectives: [], blocks: [] })}
|
||||
onClick={() => append({ title: "", description: "", objectives: [], blocks: [], requirements: [] })}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add Lesson
|
||||
</Button>
|
||||
@@ -239,6 +243,9 @@ function StepPageBuilder({ control, setValue }) {
|
||||
return next;
|
||||
});
|
||||
|
||||
const requirements = activeLesson?.requirements ?? [];
|
||||
const setRequirements = (next) => setValue(`lessons.${activeIndex}.requirements`, next, { shouldDirty: true });
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{lessons.map((l, i) => (
|
||||
@@ -247,6 +254,7 @@ function StepPageBuilder({ control, setValue }) {
|
||||
<p className="text-sm font-medium">{l.title || `Lesson ${i + 1}`}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(l.blocks?.length ?? 0)} block{(l.blocks?.length ?? 0) !== 1 ? "s" : ""}
|
||||
{(l.requirements?.length ?? 0) > 0 && ` · ${l.requirements.length} requirement${l.requirements.length !== 1 ? "s" : ""}`}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -313,6 +321,21 @@ function StepPageBuilder({ control, setValue }) {
|
||||
</PreviewChrome>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-4 border-t space-y-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Completion Requirements — {activeLesson?.title || `Lesson ${activeIndex + 1}`}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
What this specific lesson's blocks unlock — e.g. a video block above makes "Finish Watching the Full Video" available.
|
||||
</p>
|
||||
</div>
|
||||
<DraftRequirementsEditor
|
||||
entityType="lesson"
|
||||
items={requirements}
|
||||
onChange={setRequirements}
|
||||
blockTypes={blocks.map((b) => b.type)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DrawerFooter className="border-t flex-row justify-end">
|
||||
@@ -337,7 +360,7 @@ function SummaryRow({ label, value }) {
|
||||
);
|
||||
}
|
||||
|
||||
function StepReview({ data, tierCategories }) {
|
||||
function StepReview({ data, tierCategories, requirements }) {
|
||||
const tierName = tierCategories.find((c) => c.slug === data.subscription)?.name;
|
||||
|
||||
return (
|
||||
@@ -364,12 +387,26 @@ function StepReview({ data, tierCategories }) {
|
||||
<div key={i} className="flex items-center justify-between text-sm border-t border-border pt-2 first:border-t-0 first:pt-0">
|
||||
<span>{i + 1}. {l.title}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{(l.objectives ?? []).filter((o) => o.value).length} objective(s) · {(l.blocks ?? []).length} block(s)
|
||||
{(l.objectives ?? []).filter((o) => o.value).length} objective(s) · {(l.blocks ?? []).length} block(s) · {(l.requirements ?? []).length} requirement(s)
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DraftRequirementsSummary entityType="unit" items={requirements} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Step 5 — Requirements ──────────────────────────────────────────────────────
|
||||
function StepRequirements({ requirements, setRequirements }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure how learners complete this unit — optional, sensible defaults apply automatically. This is created together with the rest of the unit when you finish.
|
||||
</p>
|
||||
<DraftRequirementsEditor entityType="unit" items={requirements} onChange={setRequirements} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -380,7 +417,10 @@ export default function AddLibraryUnit() {
|
||||
const { createUnitFull, loading } = useLibrary();
|
||||
const { user } = useAuth();
|
||||
|
||||
const { syncUnitRequirements } = useCourses();
|
||||
|
||||
const [step, setStep] = useState(0);
|
||||
const [requirements, setRequirements] = useState([]);
|
||||
const [tierCategories, setTierCategories] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -398,7 +438,7 @@ export default function AddLibraryUnit() {
|
||||
mode: "onTouched",
|
||||
});
|
||||
|
||||
const { bypassOnce, dialog: unsavedChangesDialog } = useUnsavedChangesGuard(isDirty);
|
||||
const { bypassOnce, dialog: unsavedChangesDialog } = useUnsavedChangesGuard(isDirty || requirements.length > 0);
|
||||
|
||||
const handleNext = async () => {
|
||||
const fields = STEP_FIELDS[step];
|
||||
@@ -411,8 +451,9 @@ export default function AddLibraryUnit() {
|
||||
else setStep((s) => s - 1);
|
||||
};
|
||||
|
||||
// Called manually on click — no <form> tag, so no accidental submit from
|
||||
// Enter or another button while stepping through the wizard.
|
||||
// The one and only persistence point — nothing is created until this final
|
||||
// click at Review, so Requirements (the step before it) is purely a draft
|
||||
// form. No <form> tag wraps the wizard, so this is invoked manually.
|
||||
const handleCreate = async () => {
|
||||
const valid = await trigger();
|
||||
if (!valid) return;
|
||||
@@ -427,6 +468,7 @@ export default function AddLibraryUnit() {
|
||||
description: l.description || null,
|
||||
objectives: (l.objectives ?? []).map((o) => o.value).filter(Boolean),
|
||||
blocks: l.blocks ?? [],
|
||||
requirements: (l.requirements ?? []).map(({ _key, ...r }) => r),
|
||||
})),
|
||||
createdBy: user?.user_id,
|
||||
};
|
||||
@@ -434,7 +476,14 @@ export default function AddLibraryUnit() {
|
||||
// Single request creates the unit, its lessons, objectives, and page
|
||||
// content in one transaction — no per-lesson/per-page follow-up calls.
|
||||
const result = await createUnitFull(payload);
|
||||
if (!result) return;
|
||||
const newUnitId = result?.data?.data?.unit_id;
|
||||
if (!newUnitId) return;
|
||||
|
||||
if (requirements.length > 0) {
|
||||
const clean = requirements.map(({ _key, ...r }) => r);
|
||||
await syncUnitRequirements(null, newUnitId, clean);
|
||||
}
|
||||
|
||||
bypassOnce();
|
||||
navigate("/admin/units");
|
||||
};
|
||||
@@ -514,7 +563,10 @@ export default function AddLibraryUnit() {
|
||||
<StepPageBuilder control={control} setValue={setValue} />
|
||||
)}
|
||||
{step === 3 && (
|
||||
<StepReview data={getValues()} tierCategories={tierCategories} />
|
||||
<StepRequirements requirements={requirements} setRequirements={setRequirements} />
|
||||
)}
|
||||
{step === 4 && (
|
||||
<StepReview data={getValues()} tierCategories={tierCategories} requirements={requirements} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -531,6 +583,9 @@ export default function AddLibraryUnit() {
|
||||
<ChevronRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
) : (
|
||||
// Only the true final step (Review) actually persists anything —
|
||||
// the unit, its lessons/page content, and any draft requirements
|
||||
// are all created together in one shot here.
|
||||
<Button type="button" onClick={handleCreate} disabled={loading}>
|
||||
{loading && <Spinner className="h-4 w-4 mr-2" />}
|
||||
Create Unit
|
||||
|
||||
@@ -6,9 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
import { useLibrary } from "@/contexts/AdminLibraryContext";
|
||||
import { useCourses } from "@/contexts/AdminCoursesContext";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { PageMeta } from "@/contexts/MetadataContext";
|
||||
import api from "@/utils/api.util";
|
||||
import CompletionRequirementBuilder from "@/modules/admin/components/courses/CompletionRequirementBuilder";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -34,6 +36,7 @@ export default function EditLibraryUnit() {
|
||||
const navigate = useNavigate();
|
||||
const { unitId } = useParams();
|
||||
const { fetchUnit, updateUnit, unit, loading } = useLibrary();
|
||||
const { fetchUnitRequirements, syncUnitRequirements } = useCourses();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [tierCategories, setTierCategories] = useState([]);
|
||||
@@ -136,6 +139,19 @@ export default function EditLibraryUnit() {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="rounded-lg border bg-card p-6 space-y-4 mt-6">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold">Completion Requirements</h2>
|
||||
<p className="text-xs text-muted-foreground">What a learner must do for this unit to count as complete, wherever it's attached.</p>
|
||||
</div>
|
||||
<CompletionRequirementBuilder
|
||||
entityType="unit"
|
||||
fetchFn={fetchUnitRequirements}
|
||||
syncFn={syncUnitRequirements}
|
||||
args={[null, unitId]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user