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
145 changes: 77 additions & 68 deletions assets/vue/components/links/LinkItem.vue
Original file line number Diff line number Diff line change
@@ -1,89 +1,92 @@
<template>
<div class="flex flex-col pb-4 xl:flex-row xl:justify-between">
<div class="pb-2">
<h6>
<a :href="link.url">
<div class="flex flex-col pb-4 xl:flex-row xl:items-start xl:justify-between">
<div class="flex items-start gap-3 pb-2 min-w-0">
<!-- Drag handle: only visible when the user can edit -->
<div
v-if="securityStore.isAuthenticated && canEdit(link)"
class="link-drag-handle select-none rounded border border-gray-20 bg-gray-10 px-2 py-1 text-gray-600 hover:text-black"
:title="t('Drag to reorder')"
>
</div>

<div class="min-w-0">
<h6 class="min-w-0">
<a
:href="link.url"
class="inline-flex items-center gap-2 min-w-0"
>
<BaseIcon
icon="link-external"
size="normal"
/>
<span class="truncate">{{ link.title }}</span>
</a>

<BaseIcon
v-if="isAllowedToEdit && link.sessionId && Number(link.sessionId) === sidValue"
class="ml-2"
icon="session-star"
size="normal"
title="Session Item"
/>

<BaseIcon
icon="link-external"
size="small"
v-if="isLinkValid.isValid"
:title="t('Link is valid')"
class="ml-2 text-green-500"
icon="check"
size="normal"
/>
{{ link.title }}
</a>
<BaseIcon
v-if="isAllowedToEdit && link.sessionId && link.sessionId === sid"
class="mr-8"
icon="session-star"
size="small"
title="Session Item"
/>
<BaseIcon
v-if="isLinkValid.isValid"
:title="t('Link is valid')"
class="text-green-500"
icon="check"
size="small"
/>
<BaseIcon
v-else-if="isLinkValid.isValid === false"
:title="t('Link is not valid')"
class="text-red-500"
icon="alert"
size="small"
/>
</h6>
<BaseIcon
v-else-if="isLinkValid.isValid === false"
:title="t('Link is not valid')"
class="ml-2 text-red-500"
icon="alert"
size="normal"
/>
</h6>
</div>
</div>

<!-- Icon-only actions (same style as categories) -->
<div
v-if="securityStore.isAuthenticated && canEdit(link)"
class="flex gap-2"
class="flex items-center gap-3 text-gray-700"
>
<BaseButton
:label="t('Check link')"
<BaseIcon
icon="check"
size="small"
type="black"
size="normal"
:title="t('Check link')"
class="hover:text-black"
@click="emit('check', link)"
/>
<BaseButton
:label="t('Edit')"
<BaseIcon
icon="edit"
size="small"
type="black"
size="normal"
:title="t('Edit')"
class="hover:text-black"
@click="emit('edit', link)"
/>
<BaseButton
<BaseIcon
:icon="isVisible(link.linkVisible) ? 'eye-on' : 'eye-off'"
:label="t('Toggle visibility')"
size="small"
type="black"
size="normal"
:title="t('Toggle visibility')"
class="hover:text-black"
@click="emit('toggle', link)"
/>
<BaseButton
:label="t('Move up')"
icon="up"
size="small"
type="black"
@click="emit('moveUp', link)"
/>
<BaseButton
:label="t('Move down')"
icon="down"
size="small"
type="black"
@click="emit('moveDown', link)"
/>
<BaseButton
:label="t('Delete')"
<BaseIcon
icon="delete"
size="small"
type="danger"
size="normal"
:title="t('Delete')"
class="hover:text-red-600"
@click="emit('delete', link)"
/>
</div>
</div>
</template>

<script setup>
import BaseButton from "../basecomponents/BaseButton.vue"
import { useI18n } from "vue-i18n"
import BaseIcon from "../basecomponents/BaseIcon.vue"
import { isVisible } from "./linkVisibility"
Expand All @@ -97,7 +100,8 @@ const securityStore = useSecurityStore()
const isCurrentTeacher = computed(() => securityStore.isCurrentTeacher)
const route = useRoute()
const { t } = useI18n()
const { cid, sid, gid } = useCidReq()
const { sid } = useCidReq()
const sidValue = computed(() => Number(route.query.sid || (sid && typeof sid === "object" ? sid.value : sid) || 0))

defineProps({
link: {
Expand All @@ -115,14 +119,19 @@ const emit = defineEmits(["check", "edit", "toggle", "moveUp", "moveDown", "dele
const isAllowedToEdit = ref(false)

const canEdit = (item) => {
const sessionId = item.sessionId
const isSessionDocument = sessionId && sessionId === sid
const isBaseCourse = !sessionId
const sessionId = item.sessionId ? Number(item.sessionId) : 0
const isSessionItem = sessionId > 0 && sessionId === sidValue.value
const isBaseCourseItem = !sessionId

return (isSessionDocument && isAllowedToEdit.value) || (isBaseCourse && !sid && isCurrentTeacher.value)
return (isSessionItem && isAllowedToEdit.value) || (isBaseCourseItem && !sidValue.value && isCurrentTeacher.value)
}

onMounted(async () => {
isAllowedToEdit.value = await checkIsAllowedToEdit(true, true, true)
try {
isAllowedToEdit.value = await checkIsAllowedToEdit(true, true, true)
} catch (error) {
console.error("Error checking edit permission for link item:", error)
isAllowedToEdit.value = false
}
})
</script>
47 changes: 42 additions & 5 deletions assets/vue/services/linkService.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,53 @@ export default {
},

/**
* @param {Number|String} linkId
* @param {Number} position
* Move link (reorder) and optionally move between categories.
*
* @param {number|string} linkId
* @param {number} position
* @param {{categoryId?: number|null, cid?: number|null, sid?: number|null}} opts
*/
moveLink: async (linkId, position) => {
const endpoint = `${ENTRYPOINT}links/${linkId}/move`
const response = await axios.put(endpoint, { position })
moveLink: async (linkId, position, opts = {}) => {
const cid = opts.cid ?? null
const sid = opts.sid ?? null

const query = []
if (cid !== null) query.push(`cid=${encodeURIComponent(cid)}`)
if (sid !== null) query.push(`sid=${encodeURIComponent(sid)}`)

const endpoint = `${ENTRYPOINT}links/${linkId}/move` + (query.length ? `?${query.join("&")}` : "")

const payload = { position }

if (Object.prototype.hasOwnProperty.call(opts, "categoryId")) {
payload.categoryId = opts.categoryId ?? 0
}

const response = await axios.put(endpoint, payload)
return response.data
},

/**
* Export links to a real PDF generated by the backend.
* Uses multipart/form-data as defined in the API resource.
*
* @param {"pdf"} format
* @param {{cid:number, sid?:number}} params
*/
exportLinks: async (format, params) => {
const endpoint = `${ENTRYPOINT}links/export?format=${encodeURIComponent(format)}`

const formData = new FormData()
formData.append("cid", String(params.cid))

if (params.sid !== undefined && params.sid !== null && Number(params.sid) > 0) {
formData.append("sid", String(params.sid))
}

// responseType 'blob' is required to download a real PDF file.
return axios.post(endpoint, formData, { responseType: "blob" })
},

/**
* @param {Number|String} linkId
*/
Expand Down
Loading
Loading