Skip to content

Commit e5f1cab

Browse files
Merge remote-tracking branch 'upstream/master' into ras-22644
2 parents 1e82b45 + 355f71b commit e5f1cab

36 files changed

+1257
-894
lines changed

assets/css/app.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,11 @@ form .field {
820820
}
821821
}
822822

823+
img.course-tool__icon {
824+
@apply w-full h-full shadow object-cover;
825+
border-radius: 0.5rem;
826+
}
827+
823828
.row {
824829
@apply flex flex-wrap -mx-4;
825830
}

assets/vue/components/basecomponents/BaseTinyEditor.vue

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -214,14 +214,15 @@ const editorConfig = computed(() => ({
214214
...defaultEditorConfig,
215215
...props.editorConfig,
216216
file_picker_callback: filePickerCallback,
217+
setup(editor) {
218+
editor.on("GetContent", (e) => {
219+
if (!e.content.includes("tiny-content")) {
220+
e.content = `<div class="tiny-content">${e.content}</div>`
221+
}
222+
})
223+
},
217224
}))
218225
219-
watch(modelValue, (newValue) => {
220-
if (newValue && !newValue.includes("tiny-content")) {
221-
modelValue.value = `<div class="tiny-content">${newValue}</div>`
222-
}
223-
})
224-
225226
async function filePickerCallback(callback, value, meta) {
226227
let url = getUrlForTinyEditor()
227228
if ("image" === meta.filetype) {

assets/vue/components/links/LinkForm.vue

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,10 @@
4848
v-if="formData.showOnHomepage"
4949
class="mt-4 space-y-4"
5050
>
51-
<div
52-
v-if="formData.customImageUrl"
53-
class="mb-2"
54-
>
51+
<div v-if="currentPreviewImage">
5552
<p class="text-gray-600 font-semibold">{{ t("Current icon") }}</p>
5653
<img
57-
:src="formData.customImageUrl"
54+
:src="currentPreviewImage"
5855
alt="Custom Image"
5956
class="w-24 h-24 object-cover rounded-xl border shadow"
6057
/>
@@ -75,6 +72,7 @@
7572
proudlyDisplayPoweredByUppy: false,
7673
height: 350,
7774
hideUploadButton: true,
75+
autoOpenFileEditor: true,
7876
note: t('Click the image to crop it (1:1 ratio, 120x120 px recommended).'),
7977
}"
8078
/>
@@ -109,7 +107,7 @@ import { RESOURCE_LINK_PUBLISHED } from "../../constants/entity/resourcelink"
109107
import linkService from "../../services/linkService"
110108
import { useRoute, useRouter } from "vue-router"
111109
import { useI18n } from "vue-i18n"
112-
import { onMounted, reactive, ref } from "vue"
110+
import { computed, onMounted, reactive, ref, watch } from "vue"
113111
import { useCidReq } from "../../composables/cidReq"
114112
import BaseButton from "../basecomponents/BaseButton.vue"
115113
import { required, url } from "@vuelidate/validators"
@@ -133,7 +131,22 @@ const { cid, sid } = useCidReq()
133131
const router = useRouter()
134132
const route = useRoute()
135133
const selectedFile = ref(null)
136-
const uppy = new Uppy({ restrictions: { maxNumberOfFiles: 1, allowedFileTypes: ["image/*"] } })
134+
const objectUrl = ref(null)
135+
136+
const currentPreviewImage = computed(() => {
137+
if (selectedFile.value) {
138+
if (objectUrl.value) window.URL.revokeObjectURL(objectUrl.value)
139+
objectUrl.value = window.URL.createObjectURL(selectedFile.value)
140+
return objectUrl.value
141+
}
142+
return formData.customImageUrl
143+
})
144+
145+
const uppy = new Uppy({
146+
restrictions: { maxNumberOfFiles: 1, allowedFileTypes: ["image/*"] },
147+
autoProceed: false,
148+
debug: false,
149+
})
137150
.use(ImageEditor, {
138151
actions: {
139152
revert: true,
@@ -142,18 +155,34 @@ const uppy = new Uppy({ restrictions: { maxNumberOfFiles: 1, allowedFileTypes: [
142155
zoomIn: true,
143156
zoomOut: true,
144157
},
158+
quality: 1,
159+
cropperOptions: {
160+
aspectRatio: 1,
161+
croppedCanvasOptions: {
162+
width: 120,
163+
height: 120,
164+
imageSmoothingEnabled: true,
165+
imageSmoothingQuality: "high",
166+
},
167+
},
145168
})
146169
.on("file-added", async (file) => {
147-
selectedFile.value = file.data
148170
formData.removeImage = false
149171
150-
const imageEditor = uppy.getPlugin("ImageEditor")
151-
if (imageEditor) {
152-
await imageEditor.openEditor(file.id)
172+
const editor = uppy.getPlugin("ImageEditor")
173+
if (editor?.openEditor) await editor.openEditor(file.id)
174+
})
175+
.on("file-editor:complete", (updatedFile) => {
176+
if (updatedFile?.data) {
177+
const uniqueName = `customicon-${Date.now()}.png`
178+
selectedFile.value = new File([updatedFile.data], uniqueName, {
179+
type: updatedFile.type || "image/png",
180+
})
153181
}
154182
})
155183
.on("file-removed", () => {
156184
selectedFile.value = null
185+
formData.removeImage = true
157186
})
158187
159188
const props = defineProps({
@@ -171,7 +200,7 @@ const resourceLinkList = ref(
171200
{
172201
sid,
173202
cid,
174-
visibility: RESOURCE_LINK_PUBLISHED, // visible by default
203+
visibility: RESOURCE_LINK_PUBLISHED,
175204
},
176205
]),
177206
)
@@ -198,6 +227,13 @@ const rules = {
198227
}
199228
const v$ = useVuelidate(rules, formData)
200229
230+
watch(selectedFile, (file, oldFile) => {
231+
if (!file && objectUrl.value) {
232+
window.URL.revokeObjectURL(objectUrl.value)
233+
objectUrl.value = null
234+
}
235+
})
236+
201237
onMounted(() => {
202238
fetchCategories()
203239
fetchLink()
@@ -279,15 +315,7 @@ const submitForm = async () => {
279315
formDataImage.append("removeImage", formData.removeImage ? "true" : "false")
280316
281317
if (selectedFile.value) {
282-
let fileToUpload = selectedFile.value
283-
284-
if (!(fileToUpload instanceof File)) {
285-
fileToUpload = new File([fileToUpload], "custom-icon.png", {
286-
type: fileToUpload.type || "image/png",
287-
})
288-
}
289-
290-
formDataImage.append("customImage", fileToUpload)
318+
formDataImage.append("customImage", selectedFile.value)
291319
}
292320
293321
await linkService.uploadImage(linkId, formDataImage)

assets/vue/components/social/SocialWallPost.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
/>
1414

1515
<div class="flex flex-col">
16-
<div v-if="null === post.userReceiver || post.sender['@id'] === post.userReceiver['@id']">
16+
<div v-if="!post.userReceiver || post.sender['@id'] === post.userReceiver?.['@id']">
1717
<BaseAppLink :to="{ name: 'SocialWall', query: { id: post.sender['@id'] } }">
1818
{{ post.sender.fullName }}
1919
</BaseAppLink>

assets/vue/composables/formatDate.js

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
import { DateTime } from "luxon"
22
import { useLocale } from "./locale"
3+
import { usePlatformConfig } from "../store/platformConfig"
4+
import { useSecurityStore } from "../store/securityStore"
35

46
export function useFormatDate() {
57
const { appParentLocale } = useLocale()
8+
const platformConfigStore = usePlatformConfig()
9+
const securityStore = useSecurityStore()
10+
11+
function getCurrentTimezone() {
12+
const allowUserTimezone = platformConfigStore.getSetting("profile.use_users_timezone") === "true"
13+
const userTimezone = securityStore.user?.timezone
14+
const platformTimezone = platformConfigStore.getSetting("platform.timezone")
15+
16+
if (allowUserTimezone && userTimezone) {
17+
return userTimezone
18+
}
19+
20+
if (platformTimezone && platformTimezone !== "false") {
21+
return platformTimezone
22+
}
23+
24+
return Intl.DateTimeFormat().resolvedOptions().timeZone
25+
}
626

7-
/**
8-
* @param {Date|string} datetime
9-
* @returns {DateTime|null}
10-
*/
1127
function getDateTimeObject(datetime) {
1228
if (!datetime) {
1329
return null
@@ -16,16 +32,18 @@ export function useFormatDate() {
1632
let dt
1733

1834
if (typeof datetime === "string") {
19-
dt = DateTime.fromISO(datetime)
20-
} else if (typeof datetime === "object") {
21-
dt = DateTime.fromJSDate(datetime)
35+
dt = DateTime.fromISO(datetime, { zone: "utc" })
36+
} else if (datetime instanceof Date) {
37+
dt = DateTime.fromJSDate(datetime, { zone: "utc" })
38+
} else {
39+
return null
2240
}
2341

2442
if (!dt.isValid) {
2543
return null
2644
}
2745

28-
return dt.setLocale(appParentLocale.value)
46+
return dt.setZone(getCurrentTimezone()).setLocale(appParentLocale.value)
2947
}
3048

3149
const abbreviatedDatetime = (datetime) =>
@@ -34,10 +52,12 @@ export function useFormatDate() {
3452
month: "long",
3553
})
3654

37-
const relativeDatetime = (datetime) => getDateTimeObject(datetime)?.toRelative()
55+
const relativeDatetime = (datetime) =>
56+
getDateTimeObject(datetime)?.toRelative()
3857

3958
return {
4059
abbreviatedDatetime,
4160
relativeDatetime,
61+
getCurrentTimezone,
4262
}
4363
}

assets/vue/utils/dates.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
1-
const { DateTime } = require("luxon")
1+
import { DateTime } from "luxon"
2+
import { useFormatDate } from "../composables/formatDate"
23

3-
const formatDateTime = function (date) {
4-
if (!date) return null
4+
const { getCurrentTimezone } = useFormatDate()
55

6-
return DateTime(date).format("DD/MM/YYYY")
6+
/**
7+
* Format a JS Date object to string using the current or provided timezone.
8+
* @param {Date} date - JavaScript Date object
9+
* @param {string} [timezone] - Optional timezone (e.g. "America/Lima")
10+
* @returns {string}
11+
*/
12+
const formatDateTime = function (date, timezone) {
13+
if (!date) return ""
14+
const tz = timezone || getCurrentTimezone()
15+
return DateTime.fromJSDate(date, { zone: "utc" }).setZone(tz).toFormat("dd/LL/yyyy HH:mm")
716
}
817

9-
const formatDateTimeFromISO = function (dateStr) {
18+
/**
19+
* Format an ISO string to readable string using the current or provided timezone.
20+
* @param {string} dateStr - ISO date string (e.g. "2025-06-17T14:00:00Z")
21+
* @param {string} [timezone] - Optional timezone
22+
* @returns {string}
23+
*/
24+
const formatDateTimeFromISO = function (dateStr, timezone) {
1025
if (!dateStr) return ""
11-
12-
return DateTime.fromISO(dateStr).toFormat("dd/LL/yyyy HH:mm")
26+
const tz = timezone || getCurrentTimezone()
27+
return DateTime.fromISO(dateStr, { zone: "utc" }).setZone(tz).toFormat("dd/LL/yyyy HH:mm")
1328
}
1429

1530
export { formatDateTime, formatDateTimeFromISO }

assets/vue/views/ccalendarevent/CCalendarEventList.vue

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,15 @@ import { useCalendarEvent } from "../../composables/calendar/calendarEvent"
150150
import resourceLinkService from "../../services/resourceLinkService"
151151
import { useSecurityStore } from "../../store/securityStore"
152152
import { useCourseSettings } from "../../store/courseSettingStore"
153+
import { DateTime } from "luxon"
153154
154155
const store = useStore()
155156
const securityStore = useSecurityStore()
156157
const confirm = useConfirm()
157158
const cidReqStore = useCidReqStore()
158159
159160
const { course, session, group } = storeToRefs(cidReqStore)
160-
161-
const { abbreviatedDatetime } = useFormatDate()
162-
161+
const { abbreviatedDatetime, getCurrentTimezone } = useFormatDate()
163162
const { showAddButton } = useCalendarActionButtons()
164163
165164
const { isEditableByUser, allowSubscribeToEvent, allowUnsubscribeToEvent } = useCalendarEvent()
@@ -235,13 +234,15 @@ async function getCalendarEvents({ start, end }) {
235234
const calendarEvents = await cCalendarEventService.findAll({ params }).then((response) => response.json())
236235
237236
return calendarEvents["hydra:member"].map((event) => {
238-
let color = event.color || "#007BFF"
237+
const timezone = getCurrentTimezone()
238+
const start = DateTime.fromISO(event.startDate, { zone: "utc" }).setZone(timezone)
239+
const end = DateTime.fromISO(event.endDate, { zone: "utc" }).setZone(timezone)
239240
240241
return {
241242
...event,
242-
start: event.startDate,
243-
end: event.endDate,
244-
color,
243+
start: start.toString(),
244+
end: end.toString(),
245+
color: event.color || "#007BFF",
245246
}
246247
})
247248
}
@@ -257,16 +258,9 @@ const showAddEventDialog = () => {
257258
258259
dialog.value = true
259260
}
260-
261-
const goToMyStudentsSchedule = () => {
262-
window.location.href = "/main/calendar/planification.php"
263-
}
264-
265-
const goToSessionPanning = () => {
266-
window.location.href = "/main/my_space/calendar_plan.php"
267-
}
268-
261+
const timezone = getCurrentTimezone()
269262
const calendarOptions = ref({
263+
timeZone: timezone,
270264
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
271265
locales: allLocales,
272266
locale: calendarLocale?.code ?? "en-GB",

public/main/gradebook/gradebook_display_certificate.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,11 @@ function confirmation() {
335335
echo '<td width="50%">'.get_lang('Score').' : '.$valueCertificate['score_certificate'].'</td>';
336336
echo '<td width="30%">'.get_lang('Date').' : '.api_convert_and_format_date($valueCertificate['created_at']).'</td>';
337337
echo '<td width="20%">';
338-
$url = api_get_path(WEB_PATH).'certificates/index.php?id='.$valueCertificate['id'].'&user_id='.$value['user_id'];
338+
$url = '';
339+
if (!empty($valueCertificate['path_certificate']) && $valueCertificate['publish']) {
340+
$hash = pathinfo($valueCertificate['path_certificate'], PATHINFO_FILENAME);
341+
$url = api_get_path(WEB_PATH).'certificates/'.$hash.'.html';
342+
}
339343
$certificateUrl = Display::url(
340344
get_lang('Certificate'),
341345
$url,

0 commit comments

Comments
 (0)