@anantagnanam/lms 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +40 -2
- package/astro/assignment-player.ts +231 -0
- package/astro/client.ts +97 -152
- package/astro/components/AssignmentFeedback.astro +32 -0
- package/astro/components/AssignmentRubric.astro +27 -0
- package/astro/components/CourseOverview.astro +25 -3
- package/astro/components/LearnerDashboard.astro +9 -0
- package/astro/components/LearningShell.astro +16 -1
- package/astro/components/LessonAvailabilityNote.astro +26 -0
- package/astro/components/LessonContent.astro +67 -0
- package/astro/components/LessonPlayer.astro +166 -99
- package/astro/components/MediaPlayer.astro +55 -4
- package/astro/draft-queue.ts +95 -0
- package/astro/media-player.ts +124 -0
- package/astro/pages/author-preview.astro +73 -0
- package/astro/pages/course.astro +10 -0
- package/astro/pages/lesson.astro +3 -0
- package/astro/pages/library.astro +173 -0
- package/astro/pages/paths.astro +73 -0
- package/astro/pages/study.astro +180 -0
- package/astro/quiz-drafts.ts +23 -0
- package/astro/quiz-player.ts +362 -0
- package/astro/study-tools.ts +386 -0
- package/astro/styles.css +30 -0
- package/astro/transcript-search.ts +56 -0
- package/dist/admin.d.ts +3 -1
- package/dist/admin.js +756 -127
- package/dist/admin.js.map +1 -1
- package/dist/assignments-CsuAjxGO.js +75 -0
- package/dist/assignments-CsuAjxGO.js.map +1 -0
- package/dist/astro.js +4 -0
- package/dist/astro.js.map +1 -1
- package/dist/{constants-C0kT94tc.js → constants-DyQbwNbP.js} +25 -3
- package/dist/constants-DyQbwNbP.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime.d.ts +129 -16
- package/dist/runtime.js +1201 -130
- package/dist/runtime.js.map +1 -1
- package/dist/schemas.d.ts +136 -3
- package/dist/schemas.js +80 -7
- package/dist/schemas.js.map +1 -1
- package/dist/server.d.ts +18 -1
- package/dist/server.js +17 -3
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +172 -4
- package/package.json +1 -1
- package/dist/constants-C0kT94tc.js.map +0 -1
- package/dist/errors-ChzJS9al.js +0 -16
- package/dist/errors-ChzJS9al.js.map +0 -1
package/dist/admin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as PLUGIN_ID, t as API_BASE } from "./constants-
|
|
1
|
+
import { r as PLUGIN_ID, t as API_BASE } from "./constants-DyQbwNbP.js";
|
|
2
2
|
import { assignmentSchema, courseDataSchema, lessonSchema, moduleSchema, questionSchema, quizSchema, settingsSchema } from "./schemas.js";
|
|
3
3
|
import { n as enrollmentCsvLines, t as enrollmentCsvHeader } from "./csv-DKq_4OWU.js";
|
|
4
4
|
import { useEffect, useState } from "react";
|
|
@@ -208,6 +208,7 @@ function PageFrame({ title, description, children }) {
|
|
|
208
208
|
["/assignments", "Assignments"],
|
|
209
209
|
["/community", "Community"],
|
|
210
210
|
["/cohorts", "Cohorts"],
|
|
211
|
+
["/paths", "Learning paths"],
|
|
211
212
|
["/reports", "Reports"],
|
|
212
213
|
["/notifications", "Notifications"],
|
|
213
214
|
["/audit", "Audit history"],
|
|
@@ -443,8 +444,48 @@ function CoursesPage() {
|
|
|
443
444
|
]
|
|
444
445
|
});
|
|
445
446
|
}
|
|
447
|
+
function CourseHistory({ courseId, disabled, restore }) {
|
|
448
|
+
const [cursor, setCursor] = useState();
|
|
449
|
+
const load = useLoad("admin/course/history", {
|
|
450
|
+
id: courseId,
|
|
451
|
+
cursor
|
|
452
|
+
});
|
|
453
|
+
return /* @__PURE__ */ jsxs("details", {
|
|
454
|
+
className: "lms-panel",
|
|
455
|
+
children: [
|
|
456
|
+
/* @__PURE__ */ jsx("summary", { children: "Saved revision history" }),
|
|
457
|
+
/* @__PURE__ */ jsx(Notice, { error: load.error }),
|
|
458
|
+
/* @__PURE__ */ jsx("p", { children: "Restore a revision into a draft, then review and publish it. Save current edits before restoring." }),
|
|
459
|
+
load.data?.items.map((item) => /* @__PURE__ */ jsxs("div", {
|
|
460
|
+
className: "lms-toolbar",
|
|
461
|
+
children: [/* @__PURE__ */ jsxs("span", { children: [
|
|
462
|
+
new Date(item.createdAt).toLocaleString(),
|
|
463
|
+
" · ",
|
|
464
|
+
item.kind,
|
|
465
|
+
" · ",
|
|
466
|
+
item.title
|
|
467
|
+
] }), /* @__PURE__ */ jsx(Button, {
|
|
468
|
+
disabled: disabled || load.loading,
|
|
469
|
+
onClick: () => restore(item.id),
|
|
470
|
+
children: "Restore as draft"
|
|
471
|
+
})]
|
|
472
|
+
}, item.id)),
|
|
473
|
+
!load.data?.items.length && /* @__PURE__ */ jsx("p", { children: "No saved history yet. Legacy courses gain history on their next save or migration." }),
|
|
474
|
+
cursor && /* @__PURE__ */ jsx(Button, {
|
|
475
|
+
onClick: () => setCursor(void 0),
|
|
476
|
+
children: "Latest revisions"
|
|
477
|
+
}),
|
|
478
|
+
load.data?.hasMore && /* @__PURE__ */ jsx(Button, {
|
|
479
|
+
disabled: load.loading,
|
|
480
|
+
onClick: () => setCursor(load.data?.cursor),
|
|
481
|
+
children: "Older revisions"
|
|
482
|
+
})
|
|
483
|
+
]
|
|
484
|
+
});
|
|
485
|
+
}
|
|
446
486
|
function CourseEditor({ initial, initialRevision, onClose }) {
|
|
447
487
|
const [course, setCourse] = useState(initial);
|
|
488
|
+
const academy = useLoad("academy");
|
|
448
489
|
const [revision, setRevision] = useState(initialRevision);
|
|
449
490
|
const [saved, setSaved] = useState(JSON.stringify(initial));
|
|
450
491
|
const [selected, setSelected] = useState(initial.modules[0]?.lessons[0]?.id ?? null);
|
|
@@ -483,21 +524,29 @@ function CourseEditor({ initial, initialRevision, onClose }) {
|
|
|
483
524
|
}))
|
|
484
525
|
}));
|
|
485
526
|
const selectedLesson = course.modules.flatMap((module) => module.lessons).find((lesson) => lesson.id === selected);
|
|
486
|
-
const
|
|
527
|
+
const acceptSaved = (result) => {
|
|
528
|
+
setCourse(result.course);
|
|
529
|
+
setRevision(result.revision);
|
|
530
|
+
setSaved(JSON.stringify(result.course));
|
|
531
|
+
};
|
|
532
|
+
const publishOrArchive = (operation) => void action.run(async () => {
|
|
533
|
+
acceptSaved(await api(`admin/course/${operation}`, {
|
|
534
|
+
id: course.id,
|
|
535
|
+
expectedRevision: revision
|
|
536
|
+
}, "POST"));
|
|
537
|
+
}, operation === "publish" ? "Saved draft published. Existing learners keep their enrolled edition." : "Course archived.");
|
|
538
|
+
const save = () => void action.run(async () => {
|
|
487
539
|
const parsed = courseDataSchema.safeParse({
|
|
488
540
|
...course,
|
|
489
|
-
status
|
|
541
|
+
status: "draft"
|
|
490
542
|
});
|
|
491
543
|
if (!parsed.success) throw new Error(parsed.error.issues.slice(0, 5).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(" "));
|
|
492
|
-
|
|
544
|
+
acceptSaved(await api("admin/course/save", {
|
|
493
545
|
id: course.id,
|
|
494
546
|
expectedRevision: revision,
|
|
495
547
|
data: parsed.data
|
|
496
|
-
}, "POST");
|
|
497
|
-
|
|
498
|
-
setRevision(result.revision);
|
|
499
|
-
setSaved(JSON.stringify(result.course));
|
|
500
|
-
}, status === "published" ? "Course published." : "Course saved.");
|
|
548
|
+
}, "POST"));
|
|
549
|
+
}, "Draft saved. Publish it when it is ready for new learners.");
|
|
501
550
|
const reorderLesson = (moduleIndex, index, delta) => {
|
|
502
551
|
const items = [...course.modules[moduleIndex].lessons];
|
|
503
552
|
const to = index + delta;
|
|
@@ -522,19 +571,37 @@ function CourseEditor({ initial, initialRevision, onClose }) {
|
|
|
522
571
|
},
|
|
523
572
|
children: "Back to courses"
|
|
524
573
|
}),
|
|
525
|
-
/* @__PURE__ */
|
|
574
|
+
/* @__PURE__ */ jsx(Button, {
|
|
526
575
|
busy: action.busy,
|
|
527
576
|
onClick: () => save(),
|
|
528
|
-
children:
|
|
577
|
+
children: "Save draft"
|
|
529
578
|
}),
|
|
530
|
-
course.
|
|
579
|
+
course.id && /* @__PURE__ */ jsx(Button, {
|
|
531
580
|
busy: action.busy,
|
|
532
|
-
|
|
533
|
-
|
|
581
|
+
disabled: dirty,
|
|
582
|
+
onClick: () => void action.run(async () => {
|
|
583
|
+
const exported = await api("admin/course/export", { id: course.id });
|
|
584
|
+
acceptSaved(await api("admin/course/import", {
|
|
585
|
+
...exported,
|
|
586
|
+
course: {
|
|
587
|
+
...exported.course,
|
|
588
|
+
title: `Copy of ${course.title}`.slice(0, 200),
|
|
589
|
+
slug: `${course.slug.slice(0, 65)}-copy-${crypto.randomUUID().slice(0, 8)}`
|
|
590
|
+
}
|
|
591
|
+
}, "POST"));
|
|
592
|
+
}, "Course duplicated as a new draft. Review its dates and access settings before publishing."),
|
|
593
|
+
children: "Duplicate as draft"
|
|
594
|
+
}),
|
|
595
|
+
/* @__PURE__ */ jsx(Button, {
|
|
596
|
+
busy: action.busy,
|
|
597
|
+
disabled: dirty,
|
|
598
|
+
onClick: () => publishOrArchive("publish"),
|
|
599
|
+
children: "Publish saved draft"
|
|
534
600
|
}),
|
|
535
601
|
course.status === "published" && /* @__PURE__ */ jsx(Button, {
|
|
536
602
|
busy: action.busy,
|
|
537
|
-
|
|
603
|
+
disabled: dirty,
|
|
604
|
+
onClick: () => publishOrArchive("archive"),
|
|
538
605
|
children: "Archive"
|
|
539
606
|
}),
|
|
540
607
|
course.id && course.status === "draft" && /* @__PURE__ */ jsx(Button, {
|
|
@@ -553,6 +620,29 @@ function CourseEditor({ initial, initialRevision, onClose }) {
|
|
|
553
620
|
})
|
|
554
621
|
]
|
|
555
622
|
}),
|
|
623
|
+
/* @__PURE__ */ jsxs("p", {
|
|
624
|
+
role: "status",
|
|
625
|
+
children: [
|
|
626
|
+
"Course visibility: ",
|
|
627
|
+
course.status,
|
|
628
|
+
".",
|
|
629
|
+
dirty ? " Save changes before publishing." : course.publishedEditionId && course.draftRevisionId !== course.publishedDraftRevisionId ? " Saved draft changes are awaiting publication." : "",
|
|
630
|
+
course.publishedEditionId ? " Publishing creates a new edition for new enrollments; existing learners keep their course material and progress." : ""
|
|
631
|
+
]
|
|
632
|
+
}),
|
|
633
|
+
course.id && /* @__PURE__ */ jsx(CourseHistory, {
|
|
634
|
+
courseId: course.id,
|
|
635
|
+
disabled: dirty || action.busy,
|
|
636
|
+
restore: (revisionId) => void action.run(async () => {
|
|
637
|
+
const result = await api("admin/course/restore", {
|
|
638
|
+
id: course.id,
|
|
639
|
+
expectedRevision: revision,
|
|
640
|
+
revisionId
|
|
641
|
+
}, "POST");
|
|
642
|
+
acceptSaved(result);
|
|
643
|
+
setSelected(result.course.modules[0]?.lessons[0]?.id ?? null);
|
|
644
|
+
}, "Revision restored into a saved draft. Review it before publishing.")
|
|
645
|
+
}, revision),
|
|
556
646
|
/* @__PURE__ */ jsx("div", {
|
|
557
647
|
className: "lms-tabs",
|
|
558
648
|
role: "tablist",
|
|
@@ -887,10 +977,44 @@ function CourseEditor({ initial, initialRevision, onClose }) {
|
|
|
887
977
|
children: "Add module"
|
|
888
978
|
})
|
|
889
979
|
]
|
|
890
|
-
}), /* @__PURE__ */
|
|
980
|
+
}), /* @__PURE__ */ jsxs(Panel, {
|
|
891
981
|
title: selectedLesson?.title ?? "Choose a lesson",
|
|
892
|
-
children: selectedLesson
|
|
982
|
+
children: [selectedLesson && /* @__PURE__ */ jsxs("div", {
|
|
983
|
+
className: "lms-actions",
|
|
984
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
985
|
+
onClick: () => {
|
|
986
|
+
const copy = {
|
|
987
|
+
...structuredClone(selectedLesson),
|
|
988
|
+
id: crypto.randomUUID(),
|
|
989
|
+
title: `Copy of ${selectedLesson.title}`.slice(0, 200)
|
|
990
|
+
};
|
|
991
|
+
patch({ modules: course.modules.map((module) => ({
|
|
992
|
+
...module,
|
|
993
|
+
lessons: module.lessons.flatMap((lesson) => lesson.id === selectedLesson.id ? [lesson, copy] : [lesson])
|
|
994
|
+
})) });
|
|
995
|
+
setSelected(copy.id);
|
|
996
|
+
},
|
|
997
|
+
children: "Duplicate lesson"
|
|
998
|
+
}), /* @__PURE__ */ jsx(Field, {
|
|
999
|
+
label: "Move lesson to module",
|
|
1000
|
+
children: /* @__PURE__ */ jsx("select", {
|
|
1001
|
+
value: course.modules.find((m) => m.lessons.some((l) => l.id === selectedLesson.id))?.id,
|
|
1002
|
+
onChange: (event) => {
|
|
1003
|
+
const target = event.target.value;
|
|
1004
|
+
patch({ modules: course.modules.map((module) => ({
|
|
1005
|
+
...module,
|
|
1006
|
+
lessons: module.id === target ? [...module.lessons, selectedLesson] : module.lessons.filter((lesson) => lesson.id !== selectedLesson.id)
|
|
1007
|
+
})) });
|
|
1008
|
+
},
|
|
1009
|
+
children: course.modules.map((module) => /* @__PURE__ */ jsx("option", {
|
|
1010
|
+
value: module.id,
|
|
1011
|
+
children: module.title
|
|
1012
|
+
}, module.id))
|
|
1013
|
+
})
|
|
1014
|
+
})]
|
|
1015
|
+
}), selectedLesson ? /* @__PURE__ */ jsx(LessonEditor, {
|
|
893
1016
|
lesson: selectedLesson,
|
|
1017
|
+
previewUrl: course.id && academy.data && JSON.parse(saved).modules.some((module) => module.lessons.some((lesson) => lesson.id === selectedLesson.id)) ? `${academy.data.basePath}/teach/courses/${encodeURIComponent(course.id)}/lessons/${encodeURIComponent(selectedLesson.id)}` : void 0,
|
|
894
1018
|
update: (data) => updateLesson(selectedLesson.id, data),
|
|
895
1019
|
remove: () => {
|
|
896
1020
|
if (window.confirm(`Remove “${selectedLesson.title}” from the draft?`)) {
|
|
@@ -901,7 +1025,7 @@ function CourseEditor({ initial, initialRevision, onClose }) {
|
|
|
901
1025
|
setSelected(null);
|
|
902
1026
|
}
|
|
903
1027
|
}
|
|
904
|
-
}) : /* @__PURE__ */ jsx("p", { children: "Add a module and a lesson to begin." })
|
|
1028
|
+
}, selectedLesson.id) : /* @__PURE__ */ jsx("p", { children: "Add a module and a lesson to begin." })]
|
|
905
1029
|
})]
|
|
906
1030
|
})
|
|
907
1031
|
]
|
|
@@ -935,11 +1059,10 @@ function ContentPicker({ value, onChange }) {
|
|
|
935
1059
|
setCollection(event.target.value);
|
|
936
1060
|
setCursor(void 0);
|
|
937
1061
|
setEntries([]);
|
|
938
|
-
onChange(null);
|
|
939
1062
|
},
|
|
940
1063
|
children: [/* @__PURE__ */ jsx("option", {
|
|
941
1064
|
value: "",
|
|
942
|
-
children: "
|
|
1065
|
+
children: "Choose a collection"
|
|
943
1066
|
}), schema.data?.map((entry) => /* @__PURE__ */ jsx("option", {
|
|
944
1067
|
value: entry.slug,
|
|
945
1068
|
children: entry.label
|
|
@@ -948,18 +1071,18 @@ function ContentPicker({ value, onChange }) {
|
|
|
948
1071
|
}), collection && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Field, {
|
|
949
1072
|
label: "Published entry",
|
|
950
1073
|
children: /* @__PURE__ */ jsxs("select", {
|
|
951
|
-
value: value?.id
|
|
952
|
-
onChange: (event) => onChange(event.target.value ? {
|
|
1074
|
+
value: value?.collection === collection ? value.id : "",
|
|
1075
|
+
onChange: (event) => event.target.value && onChange(event.target.value ? {
|
|
953
1076
|
collection,
|
|
954
1077
|
id: event.target.value,
|
|
955
|
-
field: value?.field ?? fields.find((field) => field.type === "portableText")?.slug ?? fields[0]?.slug ?? "content"
|
|
1078
|
+
field: (value?.collection === collection ? value.field : void 0) ?? fields.find((field) => field.type === "portableText")?.slug ?? fields[0]?.slug ?? "content"
|
|
956
1079
|
} : null),
|
|
957
1080
|
children: [
|
|
958
1081
|
/* @__PURE__ */ jsx("option", {
|
|
959
1082
|
value: "",
|
|
960
1083
|
children: "Choose an entry"
|
|
961
1084
|
}),
|
|
962
|
-
value && !entries.some((entry) => entry.id === value.id) && /* @__PURE__ */ jsx("option", {
|
|
1085
|
+
value?.collection === collection && !entries.some((entry) => entry.id === value.id) && /* @__PURE__ */ jsx("option", {
|
|
963
1086
|
value: value.id,
|
|
964
1087
|
children: "Current linked entry"
|
|
965
1088
|
}),
|
|
@@ -972,8 +1095,8 @@ function ContentPicker({ value, onChange }) {
|
|
|
972
1095
|
}), /* @__PURE__ */ jsx(Field, {
|
|
973
1096
|
label: "Content field",
|
|
974
1097
|
children: /* @__PURE__ */ jsxs("select", {
|
|
975
|
-
value: value?.field
|
|
976
|
-
disabled: !value,
|
|
1098
|
+
value: value?.collection === collection ? value.field : "",
|
|
1099
|
+
disabled: !value || value.collection !== collection,
|
|
977
1100
|
onChange: (event) => value && onChange({
|
|
978
1101
|
...value,
|
|
979
1102
|
field: event.target.value
|
|
@@ -994,7 +1117,84 @@ function ContentPicker({ value, onChange }) {
|
|
|
994
1117
|
})
|
|
995
1118
|
] });
|
|
996
1119
|
}
|
|
997
|
-
function
|
|
1120
|
+
function RubricEditor({ assignment, update }) {
|
|
1121
|
+
const rubric = assignment.rubric ?? [];
|
|
1122
|
+
const change = (criteria, removed = false) => {
|
|
1123
|
+
const maxPoints = criteria.length ? criteria.reduce((sum, item) => sum + item.maxPoints, 0) : assignment.maxPoints;
|
|
1124
|
+
update({
|
|
1125
|
+
...assignment,
|
|
1126
|
+
rubric: criteria,
|
|
1127
|
+
maxPoints,
|
|
1128
|
+
passingPoints: removed ? Math.min(assignment.passingPoints, maxPoints) : assignment.passingPoints
|
|
1129
|
+
});
|
|
1130
|
+
};
|
|
1131
|
+
return /* @__PURE__ */ jsxs("section", { children: [
|
|
1132
|
+
/* @__PURE__ */ jsx("h3", { children: "Grading rubric" }),
|
|
1133
|
+
/* @__PURE__ */ jsx("p", { children: "Describe what learners should demonstrate. Criterion points add up to the assignment maximum. Submitted work keeps its original rubric." }),
|
|
1134
|
+
rubric.map((criterion, index) => /* @__PURE__ */ jsxs("fieldset", {
|
|
1135
|
+
className: "lms-resource",
|
|
1136
|
+
children: [
|
|
1137
|
+
/* @__PURE__ */ jsxs("legend", { children: ["Criterion ", index + 1] }),
|
|
1138
|
+
/* @__PURE__ */ jsx(Field, {
|
|
1139
|
+
label: "Criterion title",
|
|
1140
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
1141
|
+
required: true,
|
|
1142
|
+
maxLength: 200,
|
|
1143
|
+
value: criterion.title,
|
|
1144
|
+
onChange: (event) => change(rubric.map((item) => item.id === criterion.id ? {
|
|
1145
|
+
...item,
|
|
1146
|
+
title: event.target.value
|
|
1147
|
+
} : item))
|
|
1148
|
+
})
|
|
1149
|
+
}),
|
|
1150
|
+
/* @__PURE__ */ jsx(Field, {
|
|
1151
|
+
label: "What earns these points",
|
|
1152
|
+
children: /* @__PURE__ */ jsx("textarea", {
|
|
1153
|
+
rows: 3,
|
|
1154
|
+
maxLength: 2e3,
|
|
1155
|
+
value: criterion.description,
|
|
1156
|
+
onChange: (event) => change(rubric.map((item) => item.id === criterion.id ? {
|
|
1157
|
+
...item,
|
|
1158
|
+
description: event.target.value
|
|
1159
|
+
} : item))
|
|
1160
|
+
})
|
|
1161
|
+
}),
|
|
1162
|
+
/* @__PURE__ */ jsx(Field, {
|
|
1163
|
+
label: "Maximum criterion points",
|
|
1164
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
1165
|
+
type: "number",
|
|
1166
|
+
required: true,
|
|
1167
|
+
min: 1,
|
|
1168
|
+
max: 1e3 - (assignment.maxPoints - criterion.maxPoints),
|
|
1169
|
+
step: 1,
|
|
1170
|
+
value: criterion.maxPoints,
|
|
1171
|
+
onChange: (event) => change(rubric.map((item) => item.id === criterion.id ? {
|
|
1172
|
+
...item,
|
|
1173
|
+
maxPoints: Number(event.target.value)
|
|
1174
|
+
} : item))
|
|
1175
|
+
})
|
|
1176
|
+
}),
|
|
1177
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
1178
|
+
danger: true,
|
|
1179
|
+
onClick: () => change(rubric.filter((item) => item.id !== criterion.id), true),
|
|
1180
|
+
children: ["Remove criterion ", index + 1]
|
|
1181
|
+
})
|
|
1182
|
+
]
|
|
1183
|
+
}, criterion.id)),
|
|
1184
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1185
|
+
disabled: rubric.length >= 20 || rubric.length > 0 && assignment.maxPoints >= 1e3,
|
|
1186
|
+
onClick: () => change([...rubric, {
|
|
1187
|
+
id: crypto.randomUUID(),
|
|
1188
|
+
title: "New criterion",
|
|
1189
|
+
description: "",
|
|
1190
|
+
maxPoints: rubric.length ? Math.min(10, 1e3 - assignment.maxPoints) : assignment.maxPoints
|
|
1191
|
+
}]),
|
|
1192
|
+
children: rubric.length ? "Add criterion" : "Add grading rubric"
|
|
1193
|
+
}),
|
|
1194
|
+
/* @__PURE__ */ jsx("p", { children: rubric.length ? `${rubric.length} ${rubric.length === 1 ? "criterion" : "criteria"} · ${assignment.maxPoints} points total (maximum 1000). Review the passing points when changing scores.` : "Without a rubric, graders enter a single overall score." })
|
|
1195
|
+
] });
|
|
1196
|
+
}
|
|
1197
|
+
function LessonEditor({ lesson, previewUrl, update, remove }) {
|
|
998
1198
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
999
1199
|
/* @__PURE__ */ jsxs("div", {
|
|
1000
1200
|
className: "lms-grid",
|
|
@@ -1061,7 +1261,58 @@ function LessonEditor({ lesson, update, remove }) {
|
|
|
1061
1261
|
onChange: (event) => update({ summary: event.target.value })
|
|
1062
1262
|
})
|
|
1063
1263
|
}),
|
|
1064
|
-
/* @__PURE__ */
|
|
1264
|
+
/* @__PURE__ */ jsxs("section", {
|
|
1265
|
+
className: "lms-notice",
|
|
1266
|
+
children: [
|
|
1267
|
+
/* @__PURE__ */ jsxs("strong", { children: [
|
|
1268
|
+
"Content source:",
|
|
1269
|
+
" ",
|
|
1270
|
+
lesson.contentSource ? "Published CMS content" : lesson.portableText.length ? "Imported rich content" : "Inline text"
|
|
1271
|
+
] }),
|
|
1272
|
+
previewUrl ? /* @__PURE__ */ jsxs("p", { children: [
|
|
1273
|
+
/* @__PURE__ */ jsx("a", {
|
|
1274
|
+
href: previewUrl,
|
|
1275
|
+
target: "_blank",
|
|
1276
|
+
rel: "noopener noreferrer",
|
|
1277
|
+
children: "Preview saved lesson ↗"
|
|
1278
|
+
}),
|
|
1279
|
+
" ",
|
|
1280
|
+
"· Save changes first to include them in this preview."
|
|
1281
|
+
] }) : /* @__PURE__ */ jsx("p", { children: "Save this lesson to enable its preview." }),
|
|
1282
|
+
lesson.contentSource ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1283
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
1284
|
+
"This lesson displays the published “",
|
|
1285
|
+
lesson.contentSource.field,
|
|
1286
|
+
"” field from",
|
|
1287
|
+
" ",
|
|
1288
|
+
lesson.contentSource.collection,
|
|
1289
|
+
". Local content is retained while linked."
|
|
1290
|
+
] }),
|
|
1291
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
1292
|
+
/* @__PURE__ */ jsx("a", {
|
|
1293
|
+
href: `/_emdash/admin/content/${encodeURIComponent(lesson.contentSource.collection)}/${encodeURIComponent(lesson.contentSource.id)}`,
|
|
1294
|
+
target: "_blank",
|
|
1295
|
+
rel: "noopener noreferrer",
|
|
1296
|
+
children: "Edit linked entry in EmDash ↗"
|
|
1297
|
+
}),
|
|
1298
|
+
" ",
|
|
1299
|
+
"· Publish entry changes, then publish the saved course draft to create a new learner edition."
|
|
1300
|
+
] }),
|
|
1301
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1302
|
+
onClick: () => {
|
|
1303
|
+
if (window.confirm("Unlink the CMS entry and use this lesson's retained local content? Save the course to apply.")) update({ contentSource: null });
|
|
1304
|
+
},
|
|
1305
|
+
children: "Use retained local content"
|
|
1306
|
+
})
|
|
1307
|
+
] }) : lesson.portableText.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("p", { children: "Imported rich content is read-only here. Use the saved preview to inspect it, or link a published CMS entry below to edit rich content in EmDash." }), /* @__PURE__ */ jsx(Button, {
|
|
1308
|
+
onClick: () => {
|
|
1309
|
+
if (window.confirm("Remove this lesson's imported rich content and use its retained plain text instead? This removes formatting when you save.")) update({ portableText: [] });
|
|
1310
|
+
},
|
|
1311
|
+
children: "Replace rich content with inline text"
|
|
1312
|
+
})] }) : /* @__PURE__ */ jsx("p", { children: "Edit the inline text below, or select a published CMS entry." })
|
|
1313
|
+
]
|
|
1314
|
+
}),
|
|
1315
|
+
!lesson.contentSource && lesson.portableText.length === 0 && /* @__PURE__ */ jsx(Field, {
|
|
1065
1316
|
label: "Lesson text",
|
|
1066
1317
|
children: /* @__PURE__ */ jsx("textarea", {
|
|
1067
1318
|
rows: 12,
|
|
@@ -1090,6 +1341,65 @@ function LessonEditor({ lesson, update, remove }) {
|
|
|
1090
1341
|
onChange: (event) => update({ liveStartsAt: isoDate(event.target.value) })
|
|
1091
1342
|
})
|
|
1092
1343
|
}),
|
|
1344
|
+
["video", "audio"].includes(lesson.kind) && /* @__PURE__ */ jsxs("section", { children: [
|
|
1345
|
+
/* @__PURE__ */ jsx(Field, {
|
|
1346
|
+
label: "Transcript",
|
|
1347
|
+
help: "Readable text shown alongside native and embedded media.",
|
|
1348
|
+
children: /* @__PURE__ */ jsx("textarea", {
|
|
1349
|
+
rows: 6,
|
|
1350
|
+
maxLength: 5e4,
|
|
1351
|
+
value: lesson.transcript ?? "",
|
|
1352
|
+
onChange: (event) => update({ transcript: event.target.value })
|
|
1353
|
+
})
|
|
1354
|
+
}),
|
|
1355
|
+
/* @__PURE__ */ jsx("h3", { children: "Caption tracks" }),
|
|
1356
|
+
/* @__PURE__ */ jsx("p", { children: "WebVTT tracks apply to native media. Use same-origin files or configure CORS on both media and caption hosts. Embedded players manage their own captions." }),
|
|
1357
|
+
(lesson.captions ?? []).map((track, index) => /* @__PURE__ */ jsxs("div", { children: [
|
|
1358
|
+
/* @__PURE__ */ jsx(Field, {
|
|
1359
|
+
label: `Track ${index + 1} URL`,
|
|
1360
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
1361
|
+
value: track.src,
|
|
1362
|
+
onChange: (event) => update({ captions: lesson.captions.map((item, i) => i === index ? {
|
|
1363
|
+
...item,
|
|
1364
|
+
src: event.target.value
|
|
1365
|
+
} : item) })
|
|
1366
|
+
})
|
|
1367
|
+
}),
|
|
1368
|
+
/* @__PURE__ */ jsx(Field, {
|
|
1369
|
+
label: "Language code",
|
|
1370
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
1371
|
+
value: track.language,
|
|
1372
|
+
onChange: (event) => update({ captions: lesson.captions.map((item, i) => i === index ? {
|
|
1373
|
+
...item,
|
|
1374
|
+
language: event.target.value
|
|
1375
|
+
} : item) })
|
|
1376
|
+
})
|
|
1377
|
+
}),
|
|
1378
|
+
/* @__PURE__ */ jsx(Field, {
|
|
1379
|
+
label: "Caption label",
|
|
1380
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
1381
|
+
value: track.label,
|
|
1382
|
+
onChange: (event) => update({ captions: lesson.captions.map((item, i) => i === index ? {
|
|
1383
|
+
...item,
|
|
1384
|
+
label: event.target.value
|
|
1385
|
+
} : item) })
|
|
1386
|
+
})
|
|
1387
|
+
}),
|
|
1388
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1389
|
+
onClick: () => update({ captions: lesson.captions.filter((_, i) => i !== index) }),
|
|
1390
|
+
children: "Remove track"
|
|
1391
|
+
})
|
|
1392
|
+
] }, index)),
|
|
1393
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1394
|
+
disabled: (lesson.captions?.length ?? 0) >= 20,
|
|
1395
|
+
onClick: () => update({ captions: [...lesson.captions ?? [], {
|
|
1396
|
+
src: "",
|
|
1397
|
+
language: "en",
|
|
1398
|
+
label: "English"
|
|
1399
|
+
}] }),
|
|
1400
|
+
children: "Add captions"
|
|
1401
|
+
})
|
|
1402
|
+
] }),
|
|
1093
1403
|
/* @__PURE__ */ jsxs("div", {
|
|
1094
1404
|
className: "lms-actions",
|
|
1095
1405
|
children: [/* @__PURE__ */ jsxs("label", {
|
|
@@ -1114,7 +1424,9 @@ function LessonEditor({ lesson, update, remove }) {
|
|
|
1114
1424
|
}),
|
|
1115
1425
|
/* @__PURE__ */ jsx(ContentPicker, {
|
|
1116
1426
|
value: lesson.contentSource,
|
|
1117
|
-
onChange: (contentSource) =>
|
|
1427
|
+
onChange: (contentSource) => {
|
|
1428
|
+
if (contentSource && window.confirm("Use this published CMS field as the lesson content? Local content will be retained. Save the course to apply.")) update({ contentSource });
|
|
1429
|
+
}
|
|
1118
1430
|
}, lesson.id),
|
|
1119
1431
|
lesson.kind === "quiz" && lesson.quiz && /* @__PURE__ */ jsx(QuizEditor, {
|
|
1120
1432
|
quiz: lesson.quiz,
|
|
@@ -1142,6 +1454,8 @@ function LessonEditor({ lesson, update, remove }) {
|
|
|
1142
1454
|
children: /* @__PURE__ */ jsx("input", {
|
|
1143
1455
|
type: "number",
|
|
1144
1456
|
min: 1,
|
|
1457
|
+
max: 1e3,
|
|
1458
|
+
readOnly: Boolean(lesson.assignment.rubric?.length),
|
|
1145
1459
|
value: lesson.assignment.maxPoints,
|
|
1146
1460
|
onChange: (event) => update({ assignment: {
|
|
1147
1461
|
...lesson.assignment,
|
|
@@ -1154,6 +1468,7 @@ function LessonEditor({ lesson, update, remove }) {
|
|
|
1154
1468
|
children: /* @__PURE__ */ jsx("input", {
|
|
1155
1469
|
type: "number",
|
|
1156
1470
|
min: 1,
|
|
1471
|
+
max: lesson.assignment.maxPoints,
|
|
1157
1472
|
value: lesson.assignment.passingPoints,
|
|
1158
1473
|
onChange: (event) => update({ assignment: {
|
|
1159
1474
|
...lesson.assignment,
|
|
@@ -1184,6 +1499,10 @@ function LessonEditor({ lesson, update, remove }) {
|
|
|
1184
1499
|
allowResubmission: event.target.checked
|
|
1185
1500
|
} })
|
|
1186
1501
|
}), "Allow another submission after a failing grade"]
|
|
1502
|
+
}),
|
|
1503
|
+
/* @__PURE__ */ jsx(RubricEditor, {
|
|
1504
|
+
assignment: lesson.assignment,
|
|
1505
|
+
update: (assignment) => update({ assignment })
|
|
1187
1506
|
})
|
|
1188
1507
|
]
|
|
1189
1508
|
}),
|
|
@@ -1821,7 +2140,9 @@ function AssignmentsPage() {
|
|
|
1821
2140
|
}, !!courseId);
|
|
1822
2141
|
const [selected, setSelected] = useState();
|
|
1823
2142
|
const [points, setPoints] = useState(0);
|
|
2143
|
+
const [rubricScores, setRubricScores] = useState([]);
|
|
1824
2144
|
const [feedback, setFeedback] = useState("");
|
|
2145
|
+
const rubricTotal = rubricScores.reduce((sum, item) => sum + item.points, 0);
|
|
1825
2146
|
const action = useAction();
|
|
1826
2147
|
const grade = (event) => {
|
|
1827
2148
|
event.preventDefault();
|
|
@@ -1832,7 +2153,7 @@ function AssignmentsPage() {
|
|
|
1832
2153
|
lessonId: selected.lessonId,
|
|
1833
2154
|
userId: selected.userId,
|
|
1834
2155
|
submissionId: selected.id,
|
|
1835
|
-
points,
|
|
2156
|
+
...selected.rubric?.length ? { rubricScores } : { points },
|
|
1836
2157
|
feedback
|
|
1837
2158
|
}, "POST");
|
|
1838
2159
|
setSelected(void 0);
|
|
@@ -1872,7 +2193,59 @@ function AssignmentsPage() {
|
|
|
1872
2193
|
/* @__PURE__ */ jsxs("form", {
|
|
1873
2194
|
onSubmit: grade,
|
|
1874
2195
|
children: [
|
|
1875
|
-
/* @__PURE__ */
|
|
2196
|
+
selected.rubric?.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2197
|
+
/* @__PURE__ */ jsx("p", { children: "Score each criterion using the rubric saved with this submission." }),
|
|
2198
|
+
selected.rubric.map((criterion) => {
|
|
2199
|
+
const score = rubricScores.find((item) => item.criterionId === criterion.id);
|
|
2200
|
+
const updateScore = (values) => setRubricScores((items) => items.map((item) => item.criterionId === criterion.id ? {
|
|
2201
|
+
...item,
|
|
2202
|
+
...values
|
|
2203
|
+
} : item));
|
|
2204
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
2205
|
+
className: "lms-resource",
|
|
2206
|
+
children: [
|
|
2207
|
+
/* @__PURE__ */ jsx("legend", { children: criterion.title }),
|
|
2208
|
+
/* @__PURE__ */ jsx("p", {
|
|
2209
|
+
className: "lms-body",
|
|
2210
|
+
children: criterion.description
|
|
2211
|
+
}),
|
|
2212
|
+
/* @__PURE__ */ jsx(Field, {
|
|
2213
|
+
label: `Points for ${criterion.title} (out of ${criterion.maxPoints})`,
|
|
2214
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
2215
|
+
type: "number",
|
|
2216
|
+
required: true,
|
|
2217
|
+
min: 0,
|
|
2218
|
+
max: criterion.maxPoints,
|
|
2219
|
+
step: 1,
|
|
2220
|
+
value: score.points,
|
|
2221
|
+
onChange: (event) => updateScore({ points: Number(event.target.value) })
|
|
2222
|
+
})
|
|
2223
|
+
}),
|
|
2224
|
+
/* @__PURE__ */ jsx(Field, {
|
|
2225
|
+
label: `Feedback for ${criterion.title}`,
|
|
2226
|
+
children: /* @__PURE__ */ jsx("textarea", {
|
|
2227
|
+
rows: 3,
|
|
2228
|
+
maxLength: 2e3,
|
|
2229
|
+
value: score.feedback,
|
|
2230
|
+
onChange: (event) => updateScore({ feedback: event.target.value })
|
|
2231
|
+
})
|
|
2232
|
+
})
|
|
2233
|
+
]
|
|
2234
|
+
}, criterion.id);
|
|
2235
|
+
}),
|
|
2236
|
+
/* @__PURE__ */ jsxs("p", {
|
|
2237
|
+
role: "status",
|
|
2238
|
+
children: [
|
|
2239
|
+
"Total: ",
|
|
2240
|
+
rubricTotal,
|
|
2241
|
+
"/",
|
|
2242
|
+
selected.maxPoints,
|
|
2243
|
+
" · Passing grade:",
|
|
2244
|
+
" ",
|
|
2245
|
+
selected.passingPoints
|
|
2246
|
+
]
|
|
2247
|
+
})
|
|
2248
|
+
] }) : /* @__PURE__ */ jsx(Field, {
|
|
1876
2249
|
label: `Points out of ${selected.maxPoints}`,
|
|
1877
2250
|
help: `Passing grade: ${selected.passingPoints}`,
|
|
1878
2251
|
children: /* @__PURE__ */ jsx("input", {
|
|
@@ -1888,6 +2261,7 @@ function AssignmentsPage() {
|
|
|
1888
2261
|
label: "Feedback",
|
|
1889
2262
|
children: /* @__PURE__ */ jsx("textarea", {
|
|
1890
2263
|
rows: 6,
|
|
2264
|
+
maxLength: 1e4,
|
|
1891
2265
|
value: feedback,
|
|
1892
2266
|
onChange: (event) => setFeedback(event.target.value)
|
|
1893
2267
|
})
|
|
@@ -1929,18 +2303,39 @@ function AssignmentsPage() {
|
|
|
1929
2303
|
rel: "noopener noreferrer",
|
|
1930
2304
|
children: "Open attachment ↗"
|
|
1931
2305
|
}) }),
|
|
1932
|
-
item.status === "graded" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2306
|
+
item.status === "graded" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2307
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
2308
|
+
item.points,
|
|
2309
|
+
"/",
|
|
2310
|
+
item.maxPoints,
|
|
2311
|
+
" points"
|
|
2312
|
+
] }),
|
|
2313
|
+
/* @__PURE__ */ jsx("p", {
|
|
2314
|
+
className: "lms-body",
|
|
2315
|
+
children: item.feedback
|
|
2316
|
+
}),
|
|
2317
|
+
(item.rubric ?? []).map((criterion) => {
|
|
2318
|
+
const score = item.rubricScores?.find((entry) => entry.criterionId === criterion.id);
|
|
2319
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("strong", { children: [
|
|
2320
|
+
criterion.title,
|
|
2321
|
+
": ",
|
|
2322
|
+
score?.points ?? "—",
|
|
2323
|
+
"/",
|
|
2324
|
+
criterion.maxPoints
|
|
2325
|
+
] }), score?.feedback && /* @__PURE__ */ jsx("p", {
|
|
2326
|
+
className: "lms-body",
|
|
2327
|
+
children: score.feedback
|
|
2328
|
+
})] }, criterion.id);
|
|
2329
|
+
})
|
|
2330
|
+
] }) : /* @__PURE__ */ jsx(Button, {
|
|
1941
2331
|
onClick: () => {
|
|
1942
2332
|
setSelected(item);
|
|
1943
2333
|
setPoints(0);
|
|
2334
|
+
setRubricScores((item.rubric ?? []).map((criterion) => ({
|
|
2335
|
+
criterionId: criterion.id,
|
|
2336
|
+
points: 0,
|
|
2337
|
+
feedback: ""
|
|
2338
|
+
})));
|
|
1944
2339
|
setFeedback("");
|
|
1945
2340
|
},
|
|
1946
2341
|
children: "Review & grade"
|
|
@@ -2199,6 +2594,165 @@ function CohortMembershipQueue() {
|
|
|
2199
2594
|
]
|
|
2200
2595
|
});
|
|
2201
2596
|
}
|
|
2597
|
+
function PathsPage() {
|
|
2598
|
+
const [cursor, setCursor] = useState();
|
|
2599
|
+
const list = useLoad("admin/paths", { cursor });
|
|
2600
|
+
const courses = useChoices("admin/courses");
|
|
2601
|
+
const [edit, setEdit] = useState();
|
|
2602
|
+
const action = useAction();
|
|
2603
|
+
return /* @__PURE__ */ jsxs(PageFrame, {
|
|
2604
|
+
title: "Learning paths",
|
|
2605
|
+
description: "Build ordered programs. Learners retain the course list they joined; individual course enrollment still controls access.",
|
|
2606
|
+
children: [
|
|
2607
|
+
/* @__PURE__ */ jsx(Notice, {
|
|
2608
|
+
error: list.error || courses.error || action.error,
|
|
2609
|
+
message: action.message
|
|
2610
|
+
}),
|
|
2611
|
+
!edit && /* @__PURE__ */ jsx(Button, {
|
|
2612
|
+
onClick: () => setEdit({
|
|
2613
|
+
revision: null,
|
|
2614
|
+
title: "New learning path",
|
|
2615
|
+
description: "",
|
|
2616
|
+
status: "draft",
|
|
2617
|
+
courseIds: []
|
|
2618
|
+
}),
|
|
2619
|
+
children: "Create learning path"
|
|
2620
|
+
}),
|
|
2621
|
+
edit && /* @__PURE__ */ jsx(Panel, {
|
|
2622
|
+
title: edit.id ? "Edit path" : "Create path",
|
|
2623
|
+
children: /* @__PURE__ */ jsxs("form", {
|
|
2624
|
+
onSubmit: (event) => {
|
|
2625
|
+
event.preventDefault();
|
|
2626
|
+
action.run(async () => {
|
|
2627
|
+
const { id, revision, ...data } = edit;
|
|
2628
|
+
await api("admin/path/save", {
|
|
2629
|
+
id,
|
|
2630
|
+
expectedRevision: revision,
|
|
2631
|
+
data
|
|
2632
|
+
}, "POST");
|
|
2633
|
+
setEdit(void 0);
|
|
2634
|
+
list.reload();
|
|
2635
|
+
}, "Path saved. Existing learners retain their joined course lists.");
|
|
2636
|
+
},
|
|
2637
|
+
children: [
|
|
2638
|
+
/* @__PURE__ */ jsx(Field, {
|
|
2639
|
+
label: "Title",
|
|
2640
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
2641
|
+
required: true,
|
|
2642
|
+
maxLength: 200,
|
|
2643
|
+
value: edit.title,
|
|
2644
|
+
onChange: (event) => setEdit({
|
|
2645
|
+
...edit,
|
|
2646
|
+
title: event.target.value
|
|
2647
|
+
})
|
|
2648
|
+
})
|
|
2649
|
+
}),
|
|
2650
|
+
/* @__PURE__ */ jsx(Field, {
|
|
2651
|
+
label: "Description",
|
|
2652
|
+
children: /* @__PURE__ */ jsx("textarea", {
|
|
2653
|
+
maxLength: 5e3,
|
|
2654
|
+
rows: 4,
|
|
2655
|
+
value: edit.description,
|
|
2656
|
+
onChange: (event) => setEdit({
|
|
2657
|
+
...edit,
|
|
2658
|
+
description: event.target.value
|
|
2659
|
+
})
|
|
2660
|
+
})
|
|
2661
|
+
}),
|
|
2662
|
+
/* @__PURE__ */ jsx(Field, {
|
|
2663
|
+
label: "Visibility",
|
|
2664
|
+
children: /* @__PURE__ */ jsxs("select", {
|
|
2665
|
+
value: edit.status,
|
|
2666
|
+
onChange: (event) => setEdit({
|
|
2667
|
+
...edit,
|
|
2668
|
+
status: event.target.value
|
|
2669
|
+
}),
|
|
2670
|
+
children: [
|
|
2671
|
+
/* @__PURE__ */ jsx("option", {
|
|
2672
|
+
value: "draft",
|
|
2673
|
+
children: "Draft"
|
|
2674
|
+
}),
|
|
2675
|
+
/* @__PURE__ */ jsx("option", {
|
|
2676
|
+
value: "published",
|
|
2677
|
+
children: "Published"
|
|
2678
|
+
}),
|
|
2679
|
+
/* @__PURE__ */ jsx("option", {
|
|
2680
|
+
value: "archived",
|
|
2681
|
+
children: "Archived"
|
|
2682
|
+
})
|
|
2683
|
+
]
|
|
2684
|
+
})
|
|
2685
|
+
}),
|
|
2686
|
+
/* @__PURE__ */ jsx(ChoicesField, {
|
|
2687
|
+
label: "Courses",
|
|
2688
|
+
choices: courses.items.map((course) => ({
|
|
2689
|
+
id: course.id,
|
|
2690
|
+
label: `${course.title} (${course.status})`
|
|
2691
|
+
})),
|
|
2692
|
+
value: edit.courseIds,
|
|
2693
|
+
onChange: (courseIds) => setEdit({
|
|
2694
|
+
...edit,
|
|
2695
|
+
courseIds
|
|
2696
|
+
})
|
|
2697
|
+
}),
|
|
2698
|
+
courses.data?.hasMore && /* @__PURE__ */ jsx(Button, {
|
|
2699
|
+
onClick: courses.more,
|
|
2700
|
+
children: "Load more courses"
|
|
2701
|
+
}),
|
|
2702
|
+
/* @__PURE__ */ jsx("ol", { children: edit.courseIds.map((id, index) => /* @__PURE__ */ jsxs("li", { children: [courses.items.find((course) => course.id === id)?.title ?? id, /* @__PURE__ */ jsx(Button, {
|
|
2703
|
+
disabled: index === 0,
|
|
2704
|
+
"aria-label": "Move course earlier",
|
|
2705
|
+
onClick: () => {
|
|
2706
|
+
const ids = [...edit.courseIds];
|
|
2707
|
+
[ids[index], ids[index - 1]] = [ids[index - 1], ids[index]];
|
|
2708
|
+
setEdit({
|
|
2709
|
+
...edit,
|
|
2710
|
+
courseIds: ids
|
|
2711
|
+
});
|
|
2712
|
+
},
|
|
2713
|
+
children: "Move up"
|
|
2714
|
+
})] }, id)) }),
|
|
2715
|
+
/* @__PURE__ */ jsx(Button, {
|
|
2716
|
+
type: "submit",
|
|
2717
|
+
busy: action.busy,
|
|
2718
|
+
children: "Save path"
|
|
2719
|
+
}),
|
|
2720
|
+
/* @__PURE__ */ jsx(Button, {
|
|
2721
|
+
disabled: action.busy,
|
|
2722
|
+
onClick: () => setEdit(void 0),
|
|
2723
|
+
children: "Cancel"
|
|
2724
|
+
})
|
|
2725
|
+
]
|
|
2726
|
+
})
|
|
2727
|
+
}),
|
|
2728
|
+
list.data?.items.map((path) => /* @__PURE__ */ jsxs(Panel, {
|
|
2729
|
+
title: path.title,
|
|
2730
|
+
children: [/* @__PURE__ */ jsxs("p", { children: [
|
|
2731
|
+
path.status,
|
|
2732
|
+
" · ",
|
|
2733
|
+
path.courseIds.length,
|
|
2734
|
+
" courses"
|
|
2735
|
+
] }), /* @__PURE__ */ jsx(Button, {
|
|
2736
|
+
disabled: Boolean(edit),
|
|
2737
|
+
onClick: () => setEdit({
|
|
2738
|
+
id: path.id,
|
|
2739
|
+
revision: path.revision,
|
|
2740
|
+
title: path.title,
|
|
2741
|
+
description: path.description,
|
|
2742
|
+
courseIds: path.courseIds,
|
|
2743
|
+
status: path.status
|
|
2744
|
+
}),
|
|
2745
|
+
children: "Edit path"
|
|
2746
|
+
})]
|
|
2747
|
+
}, path.id)),
|
|
2748
|
+
/* @__PURE__ */ jsx(Pager, {
|
|
2749
|
+
page: list.data,
|
|
2750
|
+
next: setCursor,
|
|
2751
|
+
reset: () => setCursor(void 0)
|
|
2752
|
+
})
|
|
2753
|
+
]
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2202
2756
|
function CohortsPage() {
|
|
2203
2757
|
const [cursor, setCursor] = useState();
|
|
2204
2758
|
const list = useLoad("admin/cohorts", { cursor });
|
|
@@ -2606,6 +3160,75 @@ function AuditPage() {
|
|
|
2606
3160
|
]
|
|
2607
3161
|
});
|
|
2608
3162
|
}
|
|
3163
|
+
function MigrationPanel() {
|
|
3164
|
+
const load = useLoad("admin/migrations");
|
|
3165
|
+
const action = useAction();
|
|
3166
|
+
return /* @__PURE__ */ jsxs(Panel, { children: [
|
|
3167
|
+
/* @__PURE__ */ jsx("h2", { children: "Course edition migration" }),
|
|
3168
|
+
/* @__PURE__ */ jsx("p", { children: "Upgrade existing courses and pin enrollments to their original curriculum. Progress, assessments, grants and certificates are retained. Each batch handles up to 20 records and can be resumed after interruption." }),
|
|
3169
|
+
/* @__PURE__ */ jsx(Notice, { error: action.error || load.error || load.data?.lastError || void 0 }),
|
|
3170
|
+
load.data && /* @__PURE__ */ jsxs("p", { children: [
|
|
3171
|
+
"Phase: ",
|
|
3172
|
+
load.data.phase,
|
|
3173
|
+
" · Courses processed: ",
|
|
3174
|
+
load.data.coursesProcessed,
|
|
3175
|
+
" · Enrollments processed: ",
|
|
3176
|
+
load.data.enrollmentsProcessed
|
|
3177
|
+
] }),
|
|
3178
|
+
/* @__PURE__ */ jsx(Button, {
|
|
3179
|
+
busy: action.busy,
|
|
3180
|
+
disabled: load.loading || load.data?.phase === "complete",
|
|
3181
|
+
onClick: () => void action.run(async () => {
|
|
3182
|
+
try {
|
|
3183
|
+
await api("admin/migrations/run", {}, "POST");
|
|
3184
|
+
} finally {
|
|
3185
|
+
load.reload();
|
|
3186
|
+
}
|
|
3187
|
+
}, "Migration checkpoint saved."),
|
|
3188
|
+
children: "Run next batch"
|
|
3189
|
+
}),
|
|
3190
|
+
/* @__PURE__ */ jsx(Button, {
|
|
3191
|
+
onClick: load.reload,
|
|
3192
|
+
children: "Refresh status"
|
|
3193
|
+
}),
|
|
3194
|
+
/* @__PURE__ */ jsx(Notice, { message: action.message })
|
|
3195
|
+
] });
|
|
3196
|
+
}
|
|
3197
|
+
function HealthPanel() {
|
|
3198
|
+
const load = useLoad("admin/health");
|
|
3199
|
+
return /* @__PURE__ */ jsxs(Panel, {
|
|
3200
|
+
title: "Operational health",
|
|
3201
|
+
children: [
|
|
3202
|
+
/* @__PURE__ */ jsx(Notice, { error: load.error }),
|
|
3203
|
+
load.data && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3204
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
3205
|
+
"Plugin ",
|
|
3206
|
+
load.data.version,
|
|
3207
|
+
" · Email ",
|
|
3208
|
+
load.data.emailEnabled ? "enabled" : "disabled"
|
|
3209
|
+
] }),
|
|
3210
|
+
/* @__PURE__ */ jsx("p", {
|
|
3211
|
+
role: load.data.maintenanceStale ? "alert" : void 0,
|
|
3212
|
+
children: load.data.maintenanceStale ? "Maintenance has not completed successfully in the last 45 minutes. Check the host scheduler and logs." : "Scheduled maintenance is reporting successfully."
|
|
3213
|
+
}),
|
|
3214
|
+
/* @__PURE__ */ jsxs("p", { children: ["Last completed: ", load.data.maintenance?.completedAt ?? "No successful run reported"] }),
|
|
3215
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
3216
|
+
"Pending emails: ",
|
|
3217
|
+
load.data.pendingEmails,
|
|
3218
|
+
" · Failed emails: ",
|
|
3219
|
+
load.data.failedEmails,
|
|
3220
|
+
" · Oldest pending: ",
|
|
3221
|
+
load.data.oldestPendingAt ?? "None"
|
|
3222
|
+
] }),
|
|
3223
|
+
/* @__PURE__ */ jsxs("p", { children: ["Migration phase: ", load.data.migration?.phase ?? "Not started"] })
|
|
3224
|
+
] }),
|
|
3225
|
+
/* @__PURE__ */ jsx(Button, {
|
|
3226
|
+
onClick: load.reload,
|
|
3227
|
+
children: "Refresh health"
|
|
3228
|
+
})
|
|
3229
|
+
]
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
2609
3232
|
function SettingsPage() {
|
|
2610
3233
|
const load = useLoad("admin/settings");
|
|
2611
3234
|
const [settings, setSettings] = useState(settingsSchema.parse({}));
|
|
@@ -2623,97 +3246,102 @@ function SettingsPage() {
|
|
|
2623
3246
|
return /* @__PURE__ */ jsxs(PageFrame, {
|
|
2624
3247
|
title: "LMS settings",
|
|
2625
3248
|
description: "Configure your academy, certificates, moderation, and reminders.",
|
|
2626
|
-
children: [
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
3249
|
+
children: [
|
|
3250
|
+
/* @__PURE__ */ jsx(Notice, {
|
|
3251
|
+
error: load.error || action.error,
|
|
3252
|
+
message: action.message
|
|
3253
|
+
}),
|
|
3254
|
+
/* @__PURE__ */ jsx(MigrationPanel, {}),
|
|
3255
|
+
/* @__PURE__ */ jsx(HealthPanel, {}),
|
|
3256
|
+
/* @__PURE__ */ jsx(Panel, { children: /* @__PURE__ */ jsxs("form", {
|
|
3257
|
+
onSubmit: save,
|
|
3258
|
+
children: [
|
|
3259
|
+
/* @__PURE__ */ jsx(Field, {
|
|
3260
|
+
label: "Academy title",
|
|
3261
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
3262
|
+
value: settings.academyTitle,
|
|
3263
|
+
onChange: (event) => setSettings({
|
|
3264
|
+
...settings,
|
|
3265
|
+
academyTitle: event.target.value
|
|
3266
|
+
})
|
|
2639
3267
|
})
|
|
2640
|
-
})
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
3268
|
+
}),
|
|
3269
|
+
/* @__PURE__ */ jsx(Field, {
|
|
3270
|
+
label: "Academy introduction",
|
|
3271
|
+
children: /* @__PURE__ */ jsx("textarea", {
|
|
3272
|
+
rows: 3,
|
|
3273
|
+
value: settings.academyDescription,
|
|
3274
|
+
onChange: (event) => setSettings({
|
|
3275
|
+
...settings,
|
|
3276
|
+
academyDescription: event.target.value
|
|
3277
|
+
})
|
|
2650
3278
|
})
|
|
2651
|
-
})
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
3279
|
+
}),
|
|
3280
|
+
/* @__PURE__ */ jsx(Field, {
|
|
3281
|
+
label: "Certificate issuer",
|
|
3282
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
3283
|
+
value: settings.issuerName,
|
|
3284
|
+
onChange: (event) => setSettings({
|
|
3285
|
+
...settings,
|
|
3286
|
+
issuerName: event.target.value
|
|
3287
|
+
})
|
|
2660
3288
|
})
|
|
2661
|
-
})
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
3289
|
+
}),
|
|
3290
|
+
/* @__PURE__ */ jsx(Field, {
|
|
3291
|
+
label: "Remind inactive learners after days",
|
|
3292
|
+
help: "0 disables reminders. Scheduled maintenance must be enabled in the host.",
|
|
3293
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
3294
|
+
type: "number",
|
|
3295
|
+
min: 0,
|
|
3296
|
+
max: 90,
|
|
3297
|
+
value: settings.reminderDays,
|
|
3298
|
+
onChange: (event) => setSettings({
|
|
3299
|
+
...settings,
|
|
3300
|
+
reminderDays: Number(event.target.value)
|
|
3301
|
+
})
|
|
2674
3302
|
})
|
|
3303
|
+
}),
|
|
3304
|
+
/* @__PURE__ */ jsxs("label", {
|
|
3305
|
+
className: "lms-check",
|
|
3306
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
3307
|
+
type: "checkbox",
|
|
3308
|
+
checked: settings.moderateDiscussions,
|
|
3309
|
+
onChange: (event) => setSettings({
|
|
3310
|
+
...settings,
|
|
3311
|
+
moderateDiscussions: event.target.checked
|
|
3312
|
+
})
|
|
3313
|
+
}), "Review discussions before publishing"]
|
|
3314
|
+
}),
|
|
3315
|
+
/* @__PURE__ */ jsxs("label", {
|
|
3316
|
+
className: "lms-check",
|
|
3317
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
3318
|
+
type: "checkbox",
|
|
3319
|
+
checked: settings.moderateReviews,
|
|
3320
|
+
onChange: (event) => setSettings({
|
|
3321
|
+
...settings,
|
|
3322
|
+
moderateReviews: event.target.checked
|
|
3323
|
+
})
|
|
3324
|
+
}), "Review course reviews before publishing"]
|
|
3325
|
+
}),
|
|
3326
|
+
/* @__PURE__ */ jsxs("label", {
|
|
3327
|
+
className: "lms-check",
|
|
3328
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
3329
|
+
type: "checkbox",
|
|
3330
|
+
checked: settings.emailNotifications,
|
|
3331
|
+
onChange: (event) => setSettings({
|
|
3332
|
+
...settings,
|
|
3333
|
+
emailNotifications: event.target.checked
|
|
3334
|
+
})
|
|
3335
|
+
}), "Send notifications through the site's EmDash email transport"]
|
|
3336
|
+
}),
|
|
3337
|
+
/* @__PURE__ */ jsx(Button, {
|
|
3338
|
+
type: "submit",
|
|
3339
|
+
busy: action.busy,
|
|
3340
|
+
children: "Save settings"
|
|
2675
3341
|
})
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
children: [/* @__PURE__ */ jsx("input", {
|
|
2680
|
-
type: "checkbox",
|
|
2681
|
-
checked: settings.moderateDiscussions,
|
|
2682
|
-
onChange: (event) => setSettings({
|
|
2683
|
-
...settings,
|
|
2684
|
-
moderateDiscussions: event.target.checked
|
|
2685
|
-
})
|
|
2686
|
-
}), "Review discussions before publishing"]
|
|
2687
|
-
}),
|
|
2688
|
-
/* @__PURE__ */ jsxs("label", {
|
|
2689
|
-
className: "lms-check",
|
|
2690
|
-
children: [/* @__PURE__ */ jsx("input", {
|
|
2691
|
-
type: "checkbox",
|
|
2692
|
-
checked: settings.moderateReviews,
|
|
2693
|
-
onChange: (event) => setSettings({
|
|
2694
|
-
...settings,
|
|
2695
|
-
moderateReviews: event.target.checked
|
|
2696
|
-
})
|
|
2697
|
-
}), "Review course reviews before publishing"]
|
|
2698
|
-
}),
|
|
2699
|
-
/* @__PURE__ */ jsxs("label", {
|
|
2700
|
-
className: "lms-check",
|
|
2701
|
-
children: [/* @__PURE__ */ jsx("input", {
|
|
2702
|
-
type: "checkbox",
|
|
2703
|
-
checked: settings.emailNotifications,
|
|
2704
|
-
onChange: (event) => setSettings({
|
|
2705
|
-
...settings,
|
|
2706
|
-
emailNotifications: event.target.checked
|
|
2707
|
-
})
|
|
2708
|
-
}), "Send notifications through the site's EmDash email transport"]
|
|
2709
|
-
}),
|
|
2710
|
-
/* @__PURE__ */ jsx(Button, {
|
|
2711
|
-
type: "submit",
|
|
2712
|
-
busy: action.busy,
|
|
2713
|
-
children: "Save settings"
|
|
2714
|
-
})
|
|
2715
|
-
]
|
|
2716
|
-
}) })]
|
|
3342
|
+
]
|
|
3343
|
+
}) })
|
|
3344
|
+
]
|
|
2717
3345
|
});
|
|
2718
3346
|
}
|
|
2719
3347
|
function LearningWidget() {
|
|
@@ -2747,6 +3375,7 @@ const pages = {
|
|
|
2747
3375
|
"/assignments": AssignmentsPage,
|
|
2748
3376
|
"/community": CommunityPage,
|
|
2749
3377
|
"/cohorts": CohortsPage,
|
|
3378
|
+
"/paths": PathsPage,
|
|
2750
3379
|
"/reports": ReportsPage,
|
|
2751
3380
|
"/notifications": NotificationsPage,
|
|
2752
3381
|
"/audit": AuditPage,
|
|
@@ -2758,5 +3387,5 @@ const css = `
|
|
|
2758
3387
|
.ag-lms-admin *{box-sizing:border-box}.ag-lms-admin h1{font-size:clamp(1.6rem,3vw,2.15rem);margin:0}.ag-lms-admin h2{font-size:1.15rem;margin-block:0 .8rem}.ag-lms-admin h3{font-size:1rem}.ag-lms-admin p{margin-block:.5rem 1rem}.lms-page-header{display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-block-end:1.5rem}.lms-page-header>div>p{max-width:65ch;color:var(--text-color-kumo-subtle,#64748b)}.lms-eyebrow{font-size:.7rem;letter-spacing:.15em;font-weight:700}.lms-tabs{display:flex;gap:.3rem;flex-wrap:wrap;margin-block:1rem 1.5rem}.lms-tabs a{padding:.4rem .7rem;border-radius:.4rem;text-decoration:none;color:inherit;background:var(--color-kumo-control,#f1f5f9)}.lms-panel{background:var(--color-kumo-base,#fff);border:1px solid var(--color-kumo-line,#d1d5db);border-radius:.65rem;padding:1.3rem;margin-block-end:1rem;min-width:0}.lms-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.lms-course-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:1rem}.lms-course-grid .lms-panel{display:flex;flex-direction:column;align-items:flex-start}.lms-course-grid .lms-actions{margin-block-start:auto}.lms-metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem}.lms-metrics strong{display:block;font-size:2rem;margin-block-start:.5rem}.lms-metrics span,.lms-muted{font-size:.85rem;color:var(--text-color-kumo-subtle,#64748b)}.lms-actions,.lms-toolbar{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-block:.6rem 1rem}.lms-toolbar>.lms-field{min-width:160px;margin:0}.lms-button{display:inline-flex;justify-content:center;align-items:center;gap:.4rem;min-height:38px;padding:.5rem .8rem;border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:.4rem;background:var(--color-kumo-control,#f8fafc);color:var(--text-color-kumo-default,#1e293b);font:inherit;font-size:.875rem;font-weight:600;text-decoration:none;cursor:pointer}.lms-button:hover{background:var(--color-kumo-interact,#e2e8f0)}.lms-button:disabled{opacity:.55;cursor:default}.lms-danger{color:var(--text-color-kumo-danger,#b91c1c)}.lms-field{display:grid;gap:.35rem;margin-block-end:1rem;min-width:0;font-size:.875rem;font-weight:600}.lms-field small{font-weight:400;color:var(--text-color-kumo-subtle,#64748b)}.ag-lms-admin input:not([type=checkbox]):not([type=radio]),.ag-lms-admin textarea,.ag-lms-admin select{width:100%;border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:.35rem;background:var(--color-kumo-control,#fff);color:inherit;padding:.6rem;font:inherit;font-size:.875rem;font-weight:400}.ag-lms-admin input[type=checkbox],.ag-lms-admin input[type=radio]{width:1rem;height:1rem;accent-color:var(--color-kumo-brand,#2563eb)}.ag-lms-admin :focus-visible{outline:2px solid var(--color-kumo-focus,#2563eb);outline-offset:3px}.lms-check{display:flex;gap:.6rem;align-items:center;font-size:.9rem;margin-block:.7rem}.lms-notice{padding:.8rem 1rem;border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:.4rem;background:var(--color-kumo-tint,#f1f5f9)}.lms-error{color:var(--text-color-kumo-danger,#b91c1c);border-color:currentColor}.lms-badge{font-size:.7rem;text-transform:uppercase;font-weight:700;border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:1rem;padding:.2rem .6rem;margin-block-end:.6rem}.lms-studio{display:grid;grid-template-columns:330px minmax(0,1fr);gap:1.3rem;align-items:start}.lms-module{border-block-end:1px solid var(--color-kumo-line,#cbd5e1);padding-block:1rem}.lms-lesson-list{list-style:none;padding:0;margin:0}.lms-lesson-list li{display:flex;align-items:center;gap:.3rem}.lms-select-lesson{background:transparent;color:inherit;border:0;border-radius:.35rem;cursor:pointer;text-align:start;padding:.6rem;flex:1;font:inherit;font-size:.9rem}.lms-select-lesson[aria-current=true]{background:var(--color-kumo-control,#eff6ff);font-weight:600}.lms-select-lesson small{display:block;font-weight:400;opacity:.75}.lms-lesson-list .lms-actions{margin:0}.lms-option{display:flex;align-items:center;gap:.5rem;margin-block:.5rem}.lms-question{border-block-start:1px solid var(--color-kumo-line,#cbd5e1);padding-block:1rem}.lms-question fieldset{border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:.4rem;margin-block:1rem;padding:1rem}.lms-resource{border-block-end:1px solid var(--color-kumo-line,#cbd5e1);padding-block:1rem}.lms-file{position:absolute;width:1px!important;height:1px;opacity:0}.lms-table-wrap{overflow:auto}.lms-table{width:100%;border-collapse:collapse;font-size:.875rem}.lms-table th,.lms-table td{padding:.7rem;text-align:start;border-block-end:1px solid var(--color-kumo-line,#e5e7eb);vertical-align:top}.lms-table th{font-weight:600}.lms-table small{display:block;color:var(--text-color-kumo-subtle,#64748b)}.lms-table .lms-actions{margin:0}.lms-body{white-space:pre-wrap;overflow-wrap:anywhere}.ag-lms-admin details{padding:.7rem;border:1px solid var(--color-kumo-line,#d1d5db);border-radius:.4rem;margin-block:1rem}.ag-lms-admin summary{cursor:pointer;font-weight:600}.lms-notice:empty{display:none}@media(max-width:900px){.lms-studio{grid-template-columns:1fr}.lms-page-header{align-items:start;flex-direction:column}}@media(max-width:600px){.lms-grid{grid-template-columns:1fr}.lms-toolbar{align-items:stretch}.lms-toolbar>.lms-field{width:100%}.lms-panel{padding:1rem}}`;
|
|
2759
3388
|
|
|
2760
3389
|
//#endregion
|
|
2761
|
-
export { AssignmentsPage, AuditPage, CohortsPage, CommunityPage, CoursesPage, LearnersPage, LearningWidget, NotificationsPage, OverviewPage, ReportsPage, SettingsPage, pages, widgets };
|
|
3390
|
+
export { AssignmentsPage, AuditPage, CohortsPage, CommunityPage, CoursesPage, LearnersPage, LearningWidget, NotificationsPage, OverviewPage, PathsPage, ReportsPage, SettingsPage, pages, widgets };
|
|
2762
3391
|
//# sourceMappingURL=admin.js.map
|