Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
36 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
4a19a5e
Merge remote-tracking branch 'upstream/master' into feature/exercise-…
christianbeeznest Jun 23, 2026
ac0f6e6
Exercise: Fix hotspot select action icon
christianbeeznest Jun 23, 2026
0b5223c
Merge remote-tracking branch 'upstream/master' into feature/exercise-…
christianbeeznest Jun 28, 2026
6ed5b94
Merge remote-tracking branch 'upstream/master' into feature/exercise-…
christianbeeznest Jun 29, 2026
326955d
Exercise: Complete migration parity gaps
christianbeeznest Jun 29, 2026
b1a138d
Merge remote-tracking branch 'upstream/master' into feature/exercise-…
christianbeeznest Jul 16, 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
33 changes: 32 additions & 1 deletion assets/vue/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,37 @@ 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()

if (qp.has("lp_id") && "view" === qp.get("action")) {
return true
}

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

// Exercise creation launched from the LP add-item screen must keep the full
// course layout. Only the runtime/result embedded inside the LP player needs
// the empty layout to avoid duplicated Chamilo menus.
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,7 +273,7 @@ const layout = computed(() => {
return EmptyLayout
}

if ((qp.has("lp_id") && "view" === qp.get("action")) || (qp.has("origin") && "learnpath" === qp.get("origin"))) {
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>
5 changes: 5 additions & 0 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,6 +103,9 @@ export const chamiloIconToClass = {
"reply": "mdi mdi-reply",
"reply-all": "mdi mdi-reply-all",
"restart": "mdi mdi-restart",
"rocket": "mdi mdi-rocket",
Comment thread
christianbeeznest marked this conversation as resolved.
Outdated
"rocket-launch": "mdi mdi-rocket-launch",
"rocket-launch-outline": "mdi mdi-rocket-launch-outline",
"autolunch": "mdi mdi-rocket-launch",
"autolunch-off": "mdi mdi-rocket-launch-outline",
"select-all": "mdi mdi-select-group",
Expand Down Expand Up @@ -150,6 +154,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
92 changes: 86 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,85 @@ const titleCustomClass = computed(() => {
}
return ""
})
const isVisible = computed(() => props.tool.resourceNode.resourceLinks[0].visibility === 2)

function extractIdFromIri(value) {
if (!value) {
return 0
}

const match = String(value).match(/\/(\d+)$/)

return match ? Number(match[1]) : 0
}

function getCourseResourceNodeId() {
const courseNode = course.value?.resourceNode
const toolNode = props.tool?.resourceNode

return (
Number(courseNode?.id) ||
extractIdFromIri(courseNode?.["@id"]) ||
Number(toolNode?.parent?.id) ||
extractIdFromIri(toolNode?.parent?.["@id"]) ||
Number(toolNode?.id) ||
extractIdFromIri(toolNode?.["@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