Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
41e8ec6
Exercise: Migrate legacy exercise tool to Vue
christianbeeznest Jun 12, 2026
94bb903
Merge remote-tracking branch 'upstream/master' into feature/exercise-…
christianbeeznest Jun 12, 2026
2abe2c0
Exercise: Extend exercise Vue runtime question support
christianbeeznest Jun 12, 2026
93a49a7
Merge remote-tracking branch 'upstream/master' into feature/exercise-…
christianbeeznest Jun 12, 2026
e187f17
Exercise: Complete exercise Vue migration parity
christianbeeznest Jun 14, 2026
1bdfdaf
Exercise: Complete exercise Vue migration parity
christianbeeznest Jun 15, 2026
e947d9c
Fix exercise runtime review and uploads
christianbeeznest Jun 15, 2026
c9ff068
Restore exercise review answers flow
christianbeeznest Jun 15, 2026
49df287
Merge remote-tracking branch 'upstream/master' into feature/exercise-…
christianbeeznest Jun 15, 2026
a7c6e5d
Fix LP exercise scoring and visibility
christianbeeznest Jun 15, 2026
8ca43c1
Restore exercise correction notifications
christianbeeznest Jun 15, 2026
ee4b985
Restore exercise recalculate all
christianbeeznest Jun 15, 2026
b225577
Restore exercise report export options
christianbeeznest Jun 15, 2026
534e418
Enforce exercise runtime rules
christianbeeznest Jun 15, 2026
02446d9
Merge remote-tracking branch 'upstream/master' into feature/exercise-…
christianbeeznest Jun 15, 2026
9d7401c
Restore exercise report group filters
christianbeeznest Jun 15, 2026
32c34c9
Restore exercise category score breakdown
christianbeeznest Jun 15, 2026
66bc327
Merge remote-tracking branch 'upstream/master' into feature/exercise-…
christianbeeznest Jun 16, 2026
c0bea06
Merge remote-tracking branch 'upstream/master' into feature/exercise-…
christianbeeznest Jun 16, 2026
2083293
Exercise: Restore exercise runtime settings parity
christianbeeznest Jun 16, 2026
e5f3306
Restore exercise runtime UI helpers
christianbeeznest Jun 16, 2026
0dccc1c
Merge remote-tracking branch 'upstream/master' into feature/exercise-…
christianbeeznest Jun 16, 2026
7aef787
Exercise: Clean up exercise translation key reuse
christianbeeznest Jun 16, 2026
f0ccc1a
Merge remote-tracking branch 'upstream/master' into feature/exercise-…
christianbeeznest Jun 17, 2026
d2c2ed8
Exercise: Address PR review feedback
christianbeeznest Jun 17, 2026
2490766
Merge remote-tracking branch 'upstream/master' into feature/exercise-…
christianbeeznest Jun 18, 2026
ef41362
Exercise: Add adaptive hotspot delineation flow
christianbeeznest Jun 18, 2026
fa24da9
Exercise: Add OnlyOffice runtime editor
christianbeeznest Jun 18, 2026
2e1b272
Exercise: Add progressive adaptive mode
christianbeeznest Jun 18, 2026
fa0d8bd
Merge upstream master into exercise Vue migration
christianbeeznest Jun 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions assets/vue/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,43 @@ const isEmbeddedContext = computed(() => {
return isPickerContext.value || isIframeContext.value || isDialogContext.value
})

const isTruthyQueryValue = (value) => {
return ["1", "true", "yes", "on"].includes(String(value || "").toLowerCase())
}

const isLearnpathEmbeddedRoute = computed(() => {
const qp = queryParams.value
const origin = String(qp.get("origin") || "").toLowerCase()
const lpAction = String(qp.get("action") || "").toLowerCase()
const hasLpId = qp.has("lp_id")

// LP player/runtime screens are rendered inside the learning path player.
// They must use EmptyLayout to avoid duplicated Chamilo header/sidebar.
if (
hasLpId &&
("view" === lpAction || isTruthyQueryValue(qp.get("embedded")) || isTruthyQueryValue(qp.get("isStudentView")))
) {
return true
}

if ("learnpath" !== origin) {
return false
}

// Authoring screens launched from the LP add-item screen must keep the full
// course layout. This is used by Exercise, Forum and Survey creation flows.
if (isTruthyQueryValue(qp.get("returnToLp"))) {
return false
}

return (
qp.has("lp_init") ||
qp.has("learnpath_id") ||
qp.has("learnpath_item_id") ||
qp.has("learnpath_item_view_id")
)
})

const layout = computed(() => {
if (showAccessUrlChosserLayout.value) {
return AccessUrlChooserLayout
Expand All @@ -242,12 +279,7 @@ const layout = computed(() => {
return EmptyLayout
}

const lpAction = String(qp.get("action") || "").toLowerCase()
const isLearningPathPlayerContext =
qp.has("lp_id") &&
("view" === lpAction || "1" === qp.get("embedded") || "true" === String(qp.get("isStudentView") || ""))

if (isLearningPathPlayerContext) {
if (isLearnpathEmbeddedRoute.value) {
return EmptyLayout
}

Expand Down
7 changes: 6 additions & 1 deletion assets/vue/components/basecomponents/BaseCheckbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
<Checkbox
v-model="modelValue"
:binary="value === undefined"
:disabled="disabled"
:inputId="id"
:name="name"
:value="value"
/>
<label
:for="id"
class="ml-2 cursor-pointer"
:class="['ml-2', disabled ? 'cursor-not-allowed text-gray-50' : 'cursor-pointer']"
>{{ label }}</label
>
</div>
Expand Down Expand Up @@ -40,5 +41,9 @@ defineProps({
type: [String, Number, Object],
default: undefined,
},
disabled: {
type: Boolean,
default: false,
},
})
</script>
8 changes: 6 additions & 2 deletions assets/vue/components/basecomponents/ChamiloIcons.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const chamiloIconToClass = {
"back": "mdi mdi-arrow-left-bold-box",
"bug-check": "",
"bug-outline": "",
"broom": "mdi mdi-broom",
"catalogue-add": "mdi mdi-book-plus",
"catalogue-remove": "mdi mdi-book-minus-outline",
"calendar-plus": "mdi mdi-calendar-plus",
Expand Down Expand Up @@ -102,8 +103,10 @@ export const chamiloIconToClass = {
"reply": "mdi mdi-reply",
"reply-all": "mdi mdi-reply-all",
"restart": "mdi mdi-restart",
"autolunch": "mdi mdi-rocket-launch",
"autolunch-off": "mdi mdi-rocket-launch-outline",
"rocket-launch": "mdi mdi-rocket-launch",
"rocket-launch-outline": "mdi mdi-rocket-launch-outline",
"autolaunch": "mdi mdi-rocket-launch",
"autolaunch-off": "mdi mdi-rocket-launch-outline",
"select-all": "mdi mdi-select-group",
"send": "mdi mdi-send",
"sent": "mdi mdi-send-check",
Expand Down Expand Up @@ -150,6 +153,7 @@ export const chamiloIconToClass = {
"account-cancel": "mdi mdi-account-cancel",
"zip-pack": "mdi mdi-archive-arrow-down",
"zip-unpack": "mdi mdi-archive-arrow-up",
"clean-all": "mdi mdi-broom",
"clear-all": "mdi mdi-broom",
"qrcode": "mdi mdi-qrcode",
"minus": "mdi mdi-minus",
Expand Down
71 changes: 65 additions & 6 deletions assets/vue/components/course/CourseTool.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<BaseAppLink
:aria-labelledby="`course-tool-${tool.iid}`"
:class="cardCustomClass"
:to="tool.to"
:url="tool.url"
:to="resolvedToolTo"
:url="resolvedToolUrl"
class="course-tool__link hover:primary-gradient"
>
<svg
Expand All @@ -29,8 +29,8 @@
<BaseAppLink
:id="`course-tool-${tool.iid}`"
:class="titleCustomClass"
:to="tool.to"
:url="tool.url"
:to="resolvedToolTo"
:url="resolvedToolUrl"
class="course-tool__title"
>
{{ $t(tool.tool.titleToShow) }}
Expand Down Expand Up @@ -80,7 +80,7 @@ import { useCidReqStore } from "../../store/cidReq"
const platformConfigStore = usePlatformConfig()
const cidReqStore = useCidReqStore()

const { session } = storeToRefs(cidReqStore)
const { course, session } = storeToRefs(cidReqStore)
const { getSetting } = storeToRefs(platformConfigStore)

const isSorting = inject("isSorting")
Expand Down Expand Up @@ -126,5 +126,64 @@ const titleCustomClass = computed(() => {
}
return ""
})
const isVisible = computed(() => props.tool.resourceNode.resourceLinks[0].visibility === 2)

function getCourseResourceNodeId() {
return Number(cidReqStore.course?.resourceNode?.id) || Number(course.value?.resourceNode?.id) || 0
}
Comment thread
christianbeeznest marked this conversation as resolved.

const isExerciseTool = computed(() => {
const values = [
props.tool?.title,
props.tool?.titleToShow,
props.tool?.name,
props.tool?.tool?.title,
props.tool?.tool?.titleToShow,
props.tool?.tool?.name,
props.tool?.url,
]
.filter(Boolean)
.join(" ")
.toLowerCase()

return (
values.includes("quiz") ||
values.includes("exercise/exercise.php") ||
values.includes("/main/exercise/exercise.php") ||
values.includes("tests") ||
values.includes("exercises")
)
})

const exerciseToolRoute = computed(() => {
if (!isExerciseTool.value) {
return null
}

const nodeId = getCourseResourceNodeId()
const courseId = Number(course.value?.id) || Number(cidReqStore.course?.id) || 0

if (nodeId <= 0 || courseId <= 0) {
return null
}

return {
name: "ExerciseList",
params: { node: nodeId },
query: {
cid: courseId,
sid: Number(session.value?.id) || 0,
gid: 0,
},
}
})

const resolvedToolTo = computed(() => {
return exerciseToolRoute.value || props.tool.to || null
})

const resolvedToolUrl = computed(() => {
return exerciseToolRoute.value ? null : props.tool.url || null
})

const isVisible = computed(() => props.tool?.resourceNode?.resourceLinks?.[0]?.visibility === 2)
</script>
163 changes: 163 additions & 0 deletions assets/vue/router/exercise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
export default {
path: "/resources/exercise/:node/",
meta: {
requiresAuth: true,
showBreadcrumb: true,
tool: "exercise",
breadcrumb: "Exercises",
},
name: "exercise",
component: () => import("../components/layout/SimpleRouterViewLayout.vue"),
redirect: { name: "ExerciseList" },
children: [
{
name: "ExerciseList",
path: "",
meta: { breadcrumb: "Exercises" },
component: () => import("../views/exercise/ExerciseListView.vue"),
},
{
name: "ExerciseCreate",
path: "create",
meta: { breadcrumb: "Create exercise" },
component: () => import("../views/exercise/ExerciseConfigurationView.vue"),
},
{
name: "ExerciseCategories",
path: "categories",
meta: { breadcrumb: "Exercise categories" },
component: () => import("../views/exercise/ExerciseCategoryManagerView.vue"),
props: { categoryType: "exercise" },
},
{
name: "ExerciseQuestionCategories",
path: "question-categories",
meta: { breadcrumb: "Question categories" },
component: () => import("../views/exercise/ExerciseCategoryManagerView.vue"),
props: { categoryType: "question" },
},
{
name: "ExerciseImportAiken",
path: "import/aiken",
meta: { breadcrumb: "Import Aiken quiz" },
component: () => import("../views/exercise/ExerciseQuestionImportView.vue"),
props: { importType: "aiken" },
},
{
name: "ExerciseImportExcel",
path: "import/excel",
meta: { breadcrumb: "Import quiz from Excel" },
component: () => import("../views/exercise/ExerciseQuestionImportView.vue"),
props: { importType: "excel" },
},
{
name: "ExerciseImportQti2",
path: "import/qti2",
meta: { breadcrumb: "Import exercises QTI2" },
component: () => import("../views/exercise/ExerciseQuestionImportView.vue"),
props: { importType: "qti2" },
},
{
name: "ExerciseAiAikenGenerator",
path: "aiken-generator",
meta: { breadcrumb: "AI Aiken generator" },
component: () => import("../views/exercise/ExerciseAiAikenGeneratorView.vue"),
},
{
name: "ExerciseOverview",
path: ":exerciseId/overview",
meta: { breadcrumb: "Exercise overview" },
component: () => import("../views/exercise/ExerciseOverviewView.vue"),
},
{
name: "ExerciseEdit",
path: ":exerciseId/edit",
meta: { breadcrumb: "Edit exercise" },
component: () => import("../views/exercise/ExerciseConfigurationView.vue"),
},
{
name: "ExercisePlayer",
path: ":exerciseId/player",
meta: { breadcrumb: "Exercise player" },
component: () => import("../views/exercise/ExercisePlayerView.vue"),
},
{
name: "ExerciseResult",
path: ":exerciseId/result/:attemptId",
meta: { breadcrumb: "Exercise result" },
component: () => import("../views/exercise/ExerciseResultView.vue"),
},
{
name: "ExerciseReport",
path: ":exerciseId/report",
meta: { breadcrumb: "Learner score" },
component: () => import("../views/exercise/ExerciseReportView.vue"),
},
{
name: "ExerciseQuestionStats",
path: ":exerciseId/question-stats",
meta: { breadcrumb: "Question stats" },
component: () => import("../views/exercise/ExerciseQuestionStatsView.vue"),
},
{
name: "ExerciseReportByQuestion",
path: ":exerciseId/report-by-question",
meta: { breadcrumb: "Report by question" },
component: () => import("../views/exercise/ExerciseReportByQuestionView.vue"),
},
{
name: "ExerciseLiveResults",
path: ":exerciseId/live-results",
meta: { breadcrumb: "Live results" },
component: () => import("../views/exercise/ExerciseLiveResultsView.vue"),
},
{
name: "ExerciseGlobalQuestionSelector",
path: "questions/create",
meta: { breadcrumb: "Add a question" },
component: () => import("../views/exercise/ExerciseGlobalQuestionSelectorView.vue"),
},
{
name: "ExerciseGlobalQuestionCreate",
path: "questions/create/:questionType",
meta: { breadcrumb: "Create question" },
component: () => import("../views/exercise/ExerciseQuestionEditorView.vue"),
},
{
name: "ExerciseQuestionPool",
path: "questions/recycle",
meta: { breadcrumb: "Recycle existing questions" },
component: () => import("../views/exercise/ExerciseQuestionBankView.vue"),
},
{
name: "ExerciseGlobalQuestionEdit",
path: "questions/:questionId/edit",
meta: { breadcrumb: "Edit question" },
component: () => import("../views/exercise/ExerciseQuestionEditorView.vue"),
},
{
name: "ExerciseQuestions",
path: ":exerciseId/questions",
meta: { breadcrumb: "Questions" },
component: () => import("../views/exercise/ExerciseQuestionSelectorView.vue"),
},
{
name: "ExerciseQuestionBank",
path: ":exerciseId/question-bank",
meta: { breadcrumb: "Recycle existing questions" },
component: () => import("../views/exercise/ExerciseQuestionBankView.vue"),
},
{
name: "ExerciseQuestionCreate",
path: ":exerciseId/questions/create/:questionType",
meta: { breadcrumb: "Create question" },
component: () => import("../views/exercise/ExerciseQuestionEditorView.vue"),
},
{
name: "ExerciseQuestionEdit",
path: ":exerciseId/questions/:questionId/edit",
meta: { breadcrumb: "Edit question" },
component: () => import("../views/exercise/ExerciseQuestionEditorView.vue"),
},
],
}
Loading
Loading