Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
209 changes: 159 additions & 50 deletions assets/vue/components/lp/LpCategorySection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,43 +22,60 @@ const props = defineProps({
buildDates: { type: Function, required: false },
})
const emit = defineEmits([
"open","edit","report","settings","build",
"toggle-visible","toggle-publish","delete",
"export-scorm","export-pdf",
"reorder","toggle-auto-launch",
"open",
"edit",
"report",
"settings",
"build",
"toggle-visible",
"toggle-publish",
"delete",
"export-scorm",
"export-pdf",
"reorder",
"toggle-auto-launch",
])

const displayTitle = computed(() => props.title || t("Learning path categories"))

const localList = ref([...(props.list ?? [])])
const dragging = ref(false)

watch(() => props.list, (nv) => {
if (dragging.value) return
localList.value = [...(nv ?? [])]
}, { immediate: true })
watch(
() => props.list,
(nv) => {
if (dragging.value) return
localList.value = [...(nv ?? [])]
},
{ immediate: true },
)

function onEndCat() {
dragging.value = false
emit("reorder", localList.value.map(i => i.iid))
emit(
"reorder",
localList.value.map((i) => i.iid),
)
}

const route = useRoute()
const cid = computed(() => Number(route.query?.cid ?? 0) || undefined)
const sid = computed(() => Number(route.query?.sid ?? 0) || undefined)
const cid = computed(() => Number(route.query?.cid ?? 0) || undefined)
const sid = computed(() => Number(route.query?.sid ?? 0) || undefined)
const node = computed(() => Number(route.params?.node ?? 0) || undefined)

const goCat = (action, extraParams = {}) => {
const url = lpService.buildLegacyActionUrl(action, {
cid: cid.value, sid: sid.value, node: node.value,
cid: cid.value,
sid: sid.value,
node: node.value,
params: { id: props.category.iid, ...extraParams },
})
window.location.assign(url)
}
const onCatEdit = () => goCat("add_lp_category")
const onCatAddUsers = () => goCat("add_users_to_category")
const onCatEdit = () => goCat("add_lp_category")
const onCatAddUsers = () => goCat("add_users_to_category")
const onCatToggleVisibility = () => {
const vis = props.category.visibility ?? props.category.visible
const vis = props.category.visibility ?? props.category.visible
const next = typeof vis === "number" ? (vis ? 0 : 1) : 1
goCat("toggle_category_visibility", { new_status: next })
}
Expand All @@ -70,6 +87,12 @@ const onCatTogglePublish = () => {
goCat("toggle_category_publish", { new_status: next })
}
const onCatDelete = () => {
// Do not allow deletion if category is not empty
if (localList.value.length > 0) {
alert(t("You must move or remove all learning paths from this category before deleting it."))
return
}

const label = (props.category.title || "").trim() || t("Category")
const msg = `${t("Are you sure you want to delete")} ${label}?`
if (confirm(msg)) {
Expand All @@ -83,41 +106,82 @@ onMounted(() => {
const saved = localStorage.getItem(storageKey.value)
if (saved !== null) isOpen.value = saved === "1"
})
watch(isOpen, v => localStorage.setItem(storageKey.value, v ? "1" : "0"))
watch(isOpen, (v) => localStorage.setItem(storageKey.value, v ? "1" : "0"))
const panelId = computed(() => `cat-panel-${props.category?.iid || props.title}`)
const toggleOpen = () => { if (localList.value.length) isOpen.value = !isOpen.value }
function onChangeCat() {
emit("reorder", localList.value.map(i => i.iid))
const toggleOpen = () => {
if (localList.value.length) isOpen.value = !isOpen.value
}
</script>

<template>
<section class="relative ml-2 rounded-2xl shadow-lg">
<header class="relative bg-support-6 rounded-t-2xl flex items-center justify-between pl-0 pr-4 py-3">
<span class="pointer-events-none absolute inset-y-0 -left-1.5 w-1.5 bg-support-5 rounded-l-2xl" aria-hidden />
<span
class="pointer-events-none absolute inset-y-0 -left-1.5 w-1.5 bg-support-5 rounded-l-2xl"
aria-hidden
/>
<div class="flex items-center gap-3">
<template v-if="canEdit">
<button
class="w-8 h-8 grid place-content-center rounded-lg text-gray-50 hover:bg-gray-15 hover:text-gray-90"
:title="t('Drag to reorder')" :aria-label="t('Drag to reorder')"
:title="t('Drag to reorder')"
:aria-label="t('Drag to reorder')"
>
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" aria-hidden>
<circle cx="4" cy="3" r="1.2" /><circle cx="4" cy="7" r="1.2" /><circle cx="4" cy="11" r="1.2" />
<circle cx="10" cy="3" r="1.2" /><circle cx="10" cy="7" r="1.2" /><circle cx="10" cy="11" r="1.2" />
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="currentColor"
aria-hidden
>
<circle
cx="4"
cy="3"
r="1.2"
/>
<circle
cx="4"
cy="7"
r="1.2"
/>
<circle
cx="4"
cy="11"
r="1.2"
/>
<circle
cx="10"
cy="3"
r="1.2"
/>
<circle
cx="10"
cy="7"
r="1.2"
/>
<circle
cx="10"
cy="11"
r="1.2"
/>
</svg>
</button>
</template>
<template v-else>
<span class="inline-block w-8 h-8" aria-hidden></span>
<span
class="inline-block w-8 h-8"
aria-hidden
></span>
</template>

<h2 class="text-body-1 font-semibold text-gray-90">{{ displayTitle }}</h2>
</div>

<div class="flex items-center gap-2">
<div class="text-tiny text-gray-50">{{ localList.length }} {{ t('Learning paths') }}</div>
<div class="text-tiny text-gray-50">{{ localList.length }} {{ t("Learning paths") }}</div>

<BaseDropdownMenu v-if="canEdit"
<BaseDropdownMenu
v-if="canEdit"
:dropdown-id="`category-${category.iid}`"
class="relative z-30"
>
Expand All @@ -127,18 +191,48 @@ function onChangeCat() {
:title="t('Options')"
:aria-label="t('Options')"
>
<i class="mdi mdi-dots-vertical text-lg" aria-hidden></i>
<i
class="mdi mdi-dots-vertical text-lg"
aria-hidden
></i>
</span>
</template>
<template #menu>
<div class="absolute right-0 top-full mt-2 w-60 bg-white border border-gray-25 rounded-xl shadow-xl p-1 z-50">
<button class="w-full text-left px-3 py-2 rounded hover:bg-gray-15" @click="onCatEdit">{{ t('Edit category') }}</button>
<button class="w-full text-left px-3 py-2 rounded hover:bg-gray-15" @click="onCatAddUsers">{{ t('Subscribe users to category') }}</button>
<div
class="absolute right-0 top-full mt-2 w-60 bg-white border border-gray-25 rounded-xl shadow-xl p-1 z-50"
>
<button
class="w-full text-left px-3 py-2 rounded hover:bg-gray-15"
@click="onCatEdit"
>
{{ t("Edit category") }}
</button>
<button
class="w-full text-left px-3 py-2 rounded hover:bg-gray-15"
@click="onCatAddUsers"
>
{{ t("Subscribe users to category") }}
</button>
<div class="my-1 h-px bg-gray-15"></div>
<button class="w-full text-left px-3 py-2 rounded hover:bg-gray-15" @click="onCatToggleVisibility">{{ t('Toggle visibility') }}</button>
<button class="w-full text-left px-3 py-2 rounded hover:bg-gray-15" @click="onCatTogglePublish">{{ t('Publish / Hide') }}</button>
<button
class="w-full text-left px-3 py-2 rounded hover:bg-gray-15"
@click="onCatToggleVisibility"
>
{{ t("Toggle visibility") }}
</button>
<button
class="w-full text-left px-3 py-2 rounded hover:bg-gray-15"
@click="onCatTogglePublish"
>
{{ t("Publish / Hide") }}
</button>
<div class="my-1 h-px bg-gray-15"></div>
<button class="w-full text-left px-3 py-2 rounded hover:bg-gray-15 text-danger" @click="onCatDelete">{{ t('Delete') }}</button>
<button
class="w-full text-left px-3 py-2 rounded hover:bg-gray-15 text-danger"
@click="onCatDelete"
>
{{ t("Delete") }}
</button>
</div>
</template>
</BaseDropdownMenu>
Expand All @@ -151,16 +245,31 @@ function onChangeCat() {
:title="t('Expand') / t('Collapse')"
@click="toggleOpen"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
class="transition-transform duration-200"
:class="isOpen ? 'rotate-180' : ''">
<path d="M6 9l6 6 6-6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
class="transition-transform duration-200"
:class="isOpen ? 'rotate-180' : ''"
>
<path
d="M6 9l6 6 6-6"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
</header>

<div v-if="isOpen && localList.length" :id="panelId" class="sm:px-4 sm:pb-4 px-2 pb-2 bg-white rounded-b-2xl">
<div
v-if="isOpen && localList.length"
:id="panelId"
class="sm:px-4 sm:pb-4 px-2 pb-2 bg-white rounded-b-2xl"
>
<Draggable
v-model="localList"
item-key="iid"
Expand Down Expand Up @@ -189,17 +298,17 @@ function onChangeCat() {
:canExportPdf="canExportPdf"
:canAutoLaunch="canAutoLaunch"
:buildDates="buildDates"
@toggle-auto-launch="$emit('toggle-auto-launch', element)"
@open="$emit('open', element)"
@edit="$emit('edit', element)"
@report="$emit('report', element)"
@settings="$emit('settings', element)"
@build="$emit('build', element)"
@toggle-visible="$emit('toggle-visible', element)"
@toggle-publish="$emit('toggle-publish', element)"
@delete="$emit('delete', element)"
@export-scorm="$emit('export-scorm', element)"
@export-pdf="$emit('export-pdf', element)"
@toggle-auto-launch="$emit('toggle-auto-launch', element)"
@open="$emit('open', element)"
@edit="$emit('edit', element)"
@report="$emit('report', element)"
@settings="$emit('settings', element)"
@build="$emit('build', element)"
@toggle-visible="$emit('toggle-visible', element)"
@toggle-publish="$emit('toggle-publish', element)"
@delete="$emit('delete', element)"
@export-scorm="$emit('export-scorm', element)"
@export-pdf="$emit('export-pdf', element)"
/>
</template>
</Draggable>
Expand Down
10 changes: 5 additions & 5 deletions assets/vue/views/lp/LpList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,12 @@ function isVisibleForStudent(lp) {
const withCidSid = (url) => {
if (!url) return url
try {
const isAbs = url.startsWith('http://') || url.startsWith('https://')
const abs = isAbs ? url : (window.location.origin + url)
const isAbs = url.startsWith("http://") || url.startsWith("https://")
const abs = isAbs ? url : window.location.origin + url
const u = new URL(abs)
if (cid.value) u.searchParams.set('cid', String(cid.value))
if (sid.value) u.searchParams.set('sid', String(sid.value))
return isAbs ? u.toString() : (u.pathname + u.search)
if (cid.value) u.searchParams.set("cid", String(cid.value))
if (sid.value) u.searchParams.set("sid", String(sid.value))
return isAbs ? u.toString() : u.pathname + u.search
} catch {
return url
}
Expand Down
39 changes: 25 additions & 14 deletions public/main/lp/learnpath.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -7616,26 +7616,37 @@ public static function getCategorySessionId($id)
public static function deleteCategory(int $id): bool
{
$repo = Container::getLpCategoryRepository();
/** @var CLpCategory $category */
/** @var CLpCategory|null $category */
$category = $repo->find($id);
if ($category) {
$em = Database::getManager();
$lps = $category->getLps();

foreach ($lps as $lp) {
$lp->setCategory(null);
$em->persist($lp);
}

$course = api_get_course_entity();
$session = api_get_session_entity();
if (null === $category) {
return false;
}

$em->getRepository(ResourceLink::class)->removeByResourceInContext($category, $course, $session);
$em = Database::getManager();
$lps = $category->getLps();

return true;
// Detach all learning paths from this category
foreach ($lps as $lp) {
$lp->setCategory(null);
$em->persist($lp);
}

return false;
// Remove the resource link of this category in the current context
$course = api_get_course_entity();
$session = api_get_session_entity();

$em->getRepository(ResourceLink::class)->removeByResourceInContext(
$category,
$course,
$session
);

// Remove the category itself
$em->remove($category);
$em->flush();

return true;
}

/**
Expand Down
Loading