fix(dashboard): fix instructor avatar upload on course landing page (#745)
* fix(dashboard): fix instructor avatar upload on course landing page The crop dialog was hidden behind the full-screen (z-250) landing-page editor because bits-ui portals it to <body> at z-modal (200), and every crop triggered an unbounded loop of POST /media/image requests. - Add opt-in `elevatedDialog` prop to UploadImage that raises the crop dialog above the editor takeover; enable it in the instructor, certificate, and review landing-page editor forms. - Break the upload effect loop in instructor-form by reading `course.id` through `untrack`, so the effect no longer re-runs when the setter reassigns `course` after each unique upload URL. - Handle upload failures in instructor-form with a snackbar and always reset `isUploading`, so a failed upload no longer leaves the widget stuck and silent. * docs(email): add email notification preferences PRD Specify a Settings feature letting users enable/disable notification emails at two scopes: personal per-user preferences and org-wide admin defaults. Both gates must allow a send; transactional emails stay mandatory. Grounds storage in organization.settings and a new profile.settings jsonb, with a single preference guard at the email enqueue chokepoint.
This commit is contained in:
+1
-1
@@ -53,7 +53,7 @@
|
||||
<p class="text-center font-bold">
|
||||
{$t('course.navItem.landing_page.editor.certificate_form.upload_template')}
|
||||
</p>
|
||||
<UploadImage bind:avatar src={templateUrl} shape="rounded-none" bind:isUploading />
|
||||
<UploadImage bind:avatar src={templateUrl} shape="rounded-none" bind:isUploading elevatedDialog />
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex flex-col gap-2">
|
||||
|
||||
+11
-8
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import get from 'lodash/get';
|
||||
import { TextareaField } from '@cio/ui/custom/textarea-field';
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
@@ -8,6 +9,7 @@
|
||||
import type { Course } from '$features/course/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
@@ -36,18 +38,19 @@
|
||||
let isUploading = $state(false);
|
||||
|
||||
async function onAvatarChange(_avatar: File | undefined) {
|
||||
if (!_avatar || !course.id) return;
|
||||
const courseId = untrack(() => course.id);
|
||||
if (!_avatar || !courseId) return;
|
||||
|
||||
isUploading = true;
|
||||
const logo = await uploadImage(_avatar);
|
||||
|
||||
if (!logo) {
|
||||
try {
|
||||
imgUrl = await uploadImage(_avatar);
|
||||
} catch (error) {
|
||||
console.error('Failed to upload instructor avatar:', error);
|
||||
snackbar.error('snackbar.landing_page_settings.error.upload_failed');
|
||||
} finally {
|
||||
isUploading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
imgUrl = logo;
|
||||
isUploading = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
@@ -73,7 +76,7 @@
|
||||
{$t('course.navItem.landing_page.editor.instructor_form.upload')}
|
||||
</label>
|
||||
<div class="flex w-full justify-center">
|
||||
<UploadImage bind:avatar src={imgUrl} bind:isUploading />
|
||||
<UploadImage bind:avatar src={imgUrl} bind:isUploading elevatedDialog />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+7
-1
@@ -33,7 +33,13 @@
|
||||
</script>
|
||||
|
||||
<div class="flex w-full flex-col items-center">
|
||||
<UploadImage bind:avatar src={review.avatar_url} widthHeight="w-20 h-20" errorMessage={errors.avatar_url} />
|
||||
<UploadImage
|
||||
bind:avatar
|
||||
src={review.avatar_url}
|
||||
widthHeight="w-20 h-20"
|
||||
errorMessage={errors.avatar_url}
|
||||
elevatedDialog
|
||||
/>
|
||||
|
||||
<InputField
|
||||
className="mt-2 w-full"
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
flexDirection?: string;
|
||||
isUploading?: boolean;
|
||||
previewVariant?: 'avatar' | 'signature';
|
||||
/** Raise the crop dialog above full-screen takeover surfaces (e.g. the z-250 landing-page editor). */
|
||||
elevatedDialog?: boolean;
|
||||
change?: () => void;
|
||||
}
|
||||
|
||||
@@ -33,6 +35,7 @@
|
||||
flexDirection = 'flex-col',
|
||||
isUploading = $bindable(false),
|
||||
previewVariant = 'avatar',
|
||||
elevatedDialog = false,
|
||||
change
|
||||
}: Props = $props();
|
||||
|
||||
@@ -147,7 +150,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ImageCropper.Dialog>
|
||||
<ImageCropper.Dialog class={elevatedDialog ? 'z-[300]!' : undefined}>
|
||||
<ImageCropper.Cropper cropShape={shape === 'rounded-full' ? 'round' : 'rect'} />
|
||||
<ImageCropper.Controls>
|
||||
{#if isSignaturePreview}
|
||||
|
||||
@@ -256,7 +256,8 @@
|
||||
"file_size": "Filstørrelsen må ikke være større end 500 kb",
|
||||
"try_again": "Prøv igen",
|
||||
"label": "Upload billede",
|
||||
"fetch_error": "Fejl ved hentning af billeder fra Unsplash"
|
||||
"fetch_error": "Fejl ved hentning af billeder fra Unsplash",
|
||||
"upload_failed": "Kunne ikke uploade billede. Prøv venligst igen."
|
||||
}
|
||||
},
|
||||
"team_members": {
|
||||
|
||||
@@ -256,7 +256,8 @@
|
||||
"file_size": "Die Dateigröße darf 500 KB nicht überschreiten",
|
||||
"try_again": "Versuchen Sie es erneut",
|
||||
"label": "Bild hochladen",
|
||||
"fetch_error": "Fehler beim Abrufen von Bildern von Unsplash"
|
||||
"fetch_error": "Fehler beim Abrufen von Bildern von Unsplash",
|
||||
"upload_failed": "Bild konnte nicht hochgeladen werden. Bitte versuchen Sie es erneut."
|
||||
}
|
||||
},
|
||||
"team_members": {
|
||||
|
||||
@@ -4117,7 +4117,8 @@
|
||||
"file_size": "File Size must not be greater than 500kb",
|
||||
"try_again": "Try Again",
|
||||
"label": "Upload Image",
|
||||
"fetch_error": "Error fetching images from Unsplash"
|
||||
"fetch_error": "Error fetching images from Unsplash",
|
||||
"upload_failed": "Failed to upload image. Please try again."
|
||||
},
|
||||
"success": {
|
||||
"complete": "Complete :)"
|
||||
|
||||
@@ -256,7 +256,8 @@
|
||||
"file_size": "El tamaño del archivo no debe ser superior a 500 kb.",
|
||||
"try_again": "Inténtalo de nuevo",
|
||||
"label": "Subir imagen",
|
||||
"fetch_error": "Error al recuperar imágenes de Unsplash"
|
||||
"fetch_error": "Error al recuperar imágenes de Unsplash",
|
||||
"upload_failed": "No se pudo cargar la imagen. Por favor inténtalo de nuevo."
|
||||
}
|
||||
},
|
||||
"team_members": {
|
||||
|
||||
@@ -256,7 +256,8 @@
|
||||
"file_size": "La taille du fichier ne doit pas dépasser 500 Ko",
|
||||
"try_again": "Réessayez",
|
||||
"label": "Télécharger une image",
|
||||
"fetch_error": "Erreur lors de la récupération des images depuis Unsplash"
|
||||
"fetch_error": "Erreur lors de la récupération des images depuis Unsplash",
|
||||
"upload_failed": "Échec du téléchargement de l'image. Veuillez réessayer."
|
||||
}
|
||||
},
|
||||
"team_members": {
|
||||
|
||||
@@ -256,7 +256,8 @@
|
||||
"file_size": "फ़ाइल का आकार 500kb से अधिक नहीं होना चाहिए",
|
||||
"try_again": "पुनः प्रयास करें",
|
||||
"label": "छवि अपलोड करें",
|
||||
"fetch_error": "अनस्प्लैश से छवियाँ लाने में त्रुटि"
|
||||
"fetch_error": "अनस्प्लैश से छवियाँ लाने में त्रुटि",
|
||||
"upload_failed": "छवि अपलोड करने में विफल. कृपया पुन: प्रयास करें।"
|
||||
}
|
||||
},
|
||||
"team_members": {
|
||||
|
||||
@@ -256,7 +256,8 @@
|
||||
"file_size": "Rozmiar pliku nie może przekraczać 500 KB",
|
||||
"try_again": "Spróbuj ponownie",
|
||||
"label": "Prześlij obraz",
|
||||
"fetch_error": "Błąd podczas pobierania obrazów z Unsplash"
|
||||
"fetch_error": "Błąd podczas pobierania obrazów z Unsplash",
|
||||
"upload_failed": "Nie udało się przesłać obrazu. Spróbuj ponownie."
|
||||
}
|
||||
},
|
||||
"team_members": {
|
||||
|
||||
@@ -256,7 +256,8 @@
|
||||
"file_size": "O tamanho do arquivo não deve ser superior a 500kb",
|
||||
"try_again": "Tente novamente",
|
||||
"label": "Carregar imagem",
|
||||
"fetch_error": "Erro ao buscar imagens do Unsplash"
|
||||
"fetch_error": "Erro ao buscar imagens do Unsplash",
|
||||
"upload_failed": "Falha ao carregar a imagem. Por favor, tente novamente."
|
||||
}
|
||||
},
|
||||
"team_members": {
|
||||
|
||||
@@ -256,7 +256,8 @@
|
||||
"file_size": "Размер файла не должен превышать 500 КБ.",
|
||||
"try_again": "Попробуйте еще раз",
|
||||
"label": "Загрузить изображение",
|
||||
"fetch_error": "Ошибка при получении изображений из Unsplash."
|
||||
"fetch_error": "Ошибка при получении изображений из Unsplash.",
|
||||
"upload_failed": "Не удалось загрузить изображение. Пожалуйста, попробуйте еще раз."
|
||||
}
|
||||
},
|
||||
"team_members": {
|
||||
|
||||
@@ -256,7 +256,8 @@
|
||||
"file_size": "Kích thước tệp không được lớn hơn 500kb",
|
||||
"try_again": "Thử lại",
|
||||
"label": "Tải hình ảnh lên",
|
||||
"fetch_error": "Lỗi tìm nạp hình ảnh từ Unsplash"
|
||||
"fetch_error": "Lỗi tìm nạp hình ảnh từ Unsplash",
|
||||
"upload_failed": "Không thể tải lên hình ảnh. Vui lòng thử lại."
|
||||
}
|
||||
},
|
||||
"team_members": {
|
||||
|
||||
Reference in New Issue
Block a user