From a9a548f959381db68587f68b838272d6d3defd0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikko=20Pyykk=C3=B6?= Date: Mon, 22 May 2023 12:17:46 +0300 Subject: [PATCH] fix image drop zone message showing under image (#1189) --- backend/graphql/Course/mutations.ts | 14 ++--- .../Common/Fields/ControlledImageInput.tsx | 1 + .../Editor/Course/CourseImageForm.tsx | 11 ++-- .../Editor/Course/ImportPhotoDialog.tsx | 15 +++--- .../Dashboard/Editor/Course/serialization.ts | 3 +- .../EditorLegacy/Course/serialization.ts | 15 +++--- .../Dashboard/ImageDropzoneInput.tsx | 52 ++++++++++++------- .../components/Dashboard/ImagePreview.tsx | 7 ++- frontend/translations/courses/en.json | 2 + frontend/translations/courses/fi.json | 2 + 10 files changed, 71 insertions(+), 51 deletions(-) diff --git a/backend/graphql/Course/mutations.ts b/backend/graphql/Course/mutations.ts index 0e8cc755c..02ae9b5b9 100644 --- a/backend/graphql/Course/mutations.ts +++ b/backend/graphql/Course/mutations.ts @@ -74,11 +74,11 @@ export const CourseMutations = extendType({ "course_stats_email_id", ]), name: course.name ?? "", - photo: !!photo ? { connect: { id: photo } } : undefined, + photo: photo ? { connect: { id: photo } } : undefined, course_translations: { create: course_translations?.filter(isNotNullOrUndefined), }, - study_modules: !!study_modules + study_modules: study_modules ? { connect: study_modules.map((s) => ({ id: nullToUndefined(s?.id), @@ -90,20 +90,20 @@ export const CourseMutations = extendType({ }, course_variants: { create: course_variants ?? undefined }, course_aliases: { create: course_aliases ?? undefined }, - inherit_settings_from: !!inherit_settings_from + inherit_settings_from: inherit_settings_from ? { connect: { id: inherit_settings_from } } : undefined, - completions_handled_by: !!completions_handled_by + completions_handled_by: completions_handled_by ? { connect: { id: completions_handled_by } } : undefined, user_course_settings_visibilities: { create: user_course_settings_visibilities ?? undefined, }, // don't think these will be passed by parameter, but let's be sure - completion_email: !!completion_email_id + completion_email: completion_email_id ? { connect: { id: completion_email_id } } : undefined, - course_stats_email: !!course_stats_email_id + course_stats_email: course_stats_email_id ? { connect: { id: course_stats_email_id } } : undefined, tags: { connect: (tags ?? []).map((tag) => ({ id: tag.id })) }, @@ -236,7 +236,7 @@ export const CourseMutations = extendType({ end_date, // FIXME: disconnect removed photos? ...updatedFields, - photo: !!photo ? { connect: { id: photo } } : undefined, + photo: photo ? { connect: { id: photo } } : undefined, study_modules: studyModuleMutation, completion_email: completion_email_id ? { connect: { id: completion_email_id } } diff --git a/frontend/components/Dashboard/Editor/Common/Fields/ControlledImageInput.tsx b/frontend/components/Dashboard/Editor/Common/Fields/ControlledImageInput.tsx index 54263e4c8..691542a2b 100644 --- a/frontend/components/Dashboard/Editor/Common/Fields/ControlledImageInput.tsx +++ b/frontend/components/Dashboard/Editor/Common/Fields/ControlledImageInput.tsx @@ -31,6 +31,7 @@ function ControlledImageInputImpl(props: ControlledImageInputProps) { }} onImageLoad={onImageLoad} onImageAccepted={onImageAccepted} + thumbnail={thumbnail} > diff --git a/frontend/components/Dashboard/Editor/Course/CourseImageForm.tsx b/frontend/components/Dashboard/Editor/Course/CourseImageForm.tsx index b669c0c6e..63fcc2d2c 100644 --- a/frontend/components/Dashboard/Editor/Course/CourseImageForm.tsx +++ b/frontend/components/Dashboard/Editor/Course/CourseImageForm.tsx @@ -34,6 +34,8 @@ function CourseImageForm(props: CourseImageFormProps) { const { defaultValues } = useCourseEditorData() const [dialogOpen, setDialogOpen] = useState(false) + const thumbnail = watch("thumbnail") + const onImageLoad = useEventCallback((value: string | ArrayBuffer | null) => setValue("thumbnail", value), ) @@ -55,6 +57,8 @@ function CourseImageForm(props: CourseImageFormProps) { [defaultValues], ) + const thumbnailWithDomain = useMemo(() => addDomain(thumbnail), [thumbnail]) + const slug = watch("slug") const coursesWithPhotos = useMemo( @@ -85,10 +89,7 @@ function CourseImageForm(props: CourseImageFormProps) { {t("coursePhoto")} - + {t("importPhotoButton")} diff --git a/frontend/components/Dashboard/Editor/Course/ImportPhotoDialog.tsx b/frontend/components/Dashboard/Editor/Course/ImportPhotoDialog.tsx index 4a32e8f14..9a0092538 100644 --- a/frontend/components/Dashboard/Editor/Course/ImportPhotoDialog.tsx +++ b/frontend/components/Dashboard/Editor/Course/ImportPhotoDialog.tsx @@ -11,6 +11,7 @@ import { DialogTitle, } from "@mui/material" import { styled } from "@mui/material/styles" +import { useEventCallback } from "@mui/material/utils" import { ControlledSelect } from "../Common/Fields" import ContainedImage from "/components/Images/ContainedImage" @@ -52,10 +53,10 @@ function ImportPhotoDialog({ onClose, courses = [], }: ImportPhotoDialogProps) { - const { setValue, getValues, watch } = useFormContext() + const { setValue, watch } = useFormContext() const t = useTranslator(CoursesTranslations) - const fetchBase64 = useCallback( + const fetchBase64 = useEventCallback( (photo: ImageCoreFieldsFragment, filename: string) => { fetch(filename, { mode: "no-cors", @@ -67,13 +68,12 @@ function ImportPhotoDialog({ const file = new File([blob], photo?.name ?? "", { type: photo?.original_mimetype ?? "image/png", }) - setValue("new_photo", file) + setValue("new_photo", file, { shouldDirty: true }) }) }, - [setValue], ) - const fetchURL = useCallback( + const fetchURL = useEventCallback( (photo: ImageCoreFieldsFragment, filename: string) => { const req = new XMLHttpRequest() req.open("GET", filename, true) @@ -82,11 +82,10 @@ function ImportPhotoDialog({ const file = new File([req.response], photo?.name ?? "", { type: photo?.original_mimetype ?? "image/png", }) - setValue("new_photo", file) + setValue("new_photo", file, { shouldDirty: true }) } req.send() }, - [setValue], ) const photo = watch("import_photo") @@ -109,7 +108,7 @@ function ImportPhotoDialog({ fetchURL(selectedPhoto, filename) } onClose() - }, [photo, courses, getValues, onClose, setValue, watch]) + }, [photo, courses, onClose]) const selected = useMemo( () => courses?.find((course) => course.id === photo) ?? null, diff --git a/frontend/components/Dashboard/Editor/Course/serialization.ts b/frontend/components/Dashboard/Editor/Course/serialization.ts index c3df9dd9e..6b2354d31 100644 --- a/frontend/components/Dashboard/Editor/Course/serialization.ts +++ b/frontend/components/Dashboard/Editor/Course/serialization.ts @@ -113,8 +113,7 @@ export const toCourseForm = ({ exercise_completions_needed: course?.exercise_completions_needed ?? undefined, points_needed: course?.points_needed ?? undefined, - // TODO: fix - new_photo: null, // course?.photo ?? null, + new_photo: null, photo: course?.photo ?? "", open_university_registration_links: course?.open_university_registration_links?.map((link) => ({ diff --git a/frontend/components/Dashboard/EditorLegacy/Course/serialization.ts b/frontend/components/Dashboard/EditorLegacy/Course/serialization.ts index dc885658e..2b63ead0c 100644 --- a/frontend/components/Dashboard/EditorLegacy/Course/serialization.ts +++ b/frontend/components/Dashboard/EditorLegacy/Course/serialization.ts @@ -22,6 +22,12 @@ interface ToCourseFormArgs { modules?: StudyModuleDetailedFieldsFragment[] | null } +const statusMap: Record = { + Upcoming: CourseStatus.Upcoming, + Active: CourseStatus.Active, + Ended: CourseStatus.Ended, +} + export const toCourseForm = ({ course, modules, @@ -236,14 +242,7 @@ export function fromCourseForm({ new_slug: string }) - const status = - values.status === "Active" - ? CourseStatus.Active - : values.status === "Ended" - ? CourseStatus.Ended - : values.status === "Upcoming" - ? CourseStatus.Upcoming - : undefined + const status = statusMap[values.status] const c: FromCourseFormReturn = { ...formValues, diff --git a/frontend/components/Dashboard/ImageDropzoneInput.tsx b/frontend/components/Dashboard/ImageDropzoneInput.tsx index 71f467395..a4b3edf51 100644 --- a/frontend/components/Dashboard/ImageDropzoneInput.tsx +++ b/frontend/components/Dashboard/ImageDropzoneInput.tsx @@ -19,25 +19,31 @@ const DropzoneContainer = styled("div", { shouldForwardProp: (prop) => typeof prop !== "string" || !["isDragActive", "isDragAccept", "error"].includes(prop), // TODO: should I list _all_ dropzonestate things -})` +})( + ({ isDragActive, isDragAccept, error }) => ` display: flex; width: 100%; min-height: 250px; align-items: center; border-width: 2px; border-radius: 4px; - border-style: ${({ isDragActive }) => (isDragActive ? "solid" : "dashed")}; + border-style: ${isDragActive ? "solid" : "dashed"}; padding: 20px; - background-color: ${({ isDragActive, isDragAccept, error }) => - isDragActive - ? isDragAccept - ? "#E0FFE0" - : "#FFC0C0" - : error - ? "#FFC0C0" - : "#FFFFFF"}; - border-color: ${({ isDragActive, isDragAccept }) => - isDragActive ? (isDragAccept ? "#00A000" : "#FF0000") : "rgba(0,0,0,0.23)"}; + background-color: ${() => { + if (isDragActive) { + return isDragAccept ? "#E0FFE0" : "#FFC0C0" + } + if (error) { + return "#FFC0C0" + } + return "#FFFFFF" + }}; + border-color: ${() => { + if (isDragActive) { + return isDragAccept ? "#00A000" : "#FF0000" + } + return "rgba(0,0,0,0.23)" + }}; transition: border 0.24s ease-in-out; &:hover { cursor: pointer; @@ -45,13 +51,17 @@ const DropzoneContainer = styled("div", { } justify-content: center; position: relative; -` +`, +) const ErrorMessage = styled(Typography, { shouldForwardProp: (prop) => prop !== "error", -})<{ error: MessageProps["error"] }>` - color: ${({ error }) => (error ? "#FF0000" : "#000000")}; -` +})<{ error: MessageProps["error"] }>( + ({ error }) => ` + color: ${error ? "#FF0000" : "#000000"}; +`, +) + interface MessageProps { message: string error?: boolean @@ -61,12 +71,14 @@ interface DropzoneProps { inputRef?: React.RefCallback onImageLoad: (result: string | ArrayBuffer | null) => void onImageAccepted: (field: File) => void + thumbnail?: string } const ImageDropzoneInput = ({ inputRef, onImageAccepted, onImageLoad, + thumbnail, children, }: React.PropsWithChildren) => { const t = useTranslator(CommonTranslations) @@ -130,9 +142,11 @@ const ImageDropzoneInput = ({ > {children} - - {status.message} - + {!thumbnail && ( + + {status.message} + + )} ) } diff --git a/frontend/components/Dashboard/ImagePreview.tsx b/frontend/components/Dashboard/ImagePreview.tsx index e30b83561..624dbc86c 100644 --- a/frontend/components/Dashboard/ImagePreview.tsx +++ b/frontend/components/Dashboard/ImagePreview.tsx @@ -2,6 +2,8 @@ import { ButtonBase, Tooltip } from "@mui/material" import { styled } from "@mui/material/styles" import ContainedImage from "../Images/ContainedImage" +import { useTranslator } from "/hooks/useTranslator" +import CoursesTranslations from "/translations/courses" const CloseButton = styled(ButtonBase)` position: absolute; @@ -60,6 +62,7 @@ const ImagePreview = ({ height, ...rest }: ImagePreviewProps) => { + const t = useTranslator(CoursesTranslations) if (!file) { return null } @@ -68,11 +71,11 @@ const ImagePreview = ({ 64 ? "Image preview" : file} // don't spout gibberish if it's a base64 + alt={file.length > 64 ? t("coursePhotoPreview") : file} // don't spout gibberish if it's a base64 fill /> {onImageRemove && ( - + × )} diff --git a/frontend/translations/courses/en.json b/frontend/translations/courses/en.json index 980c834b1..9f5530df6 100644 --- a/frontend/translations/courses/en.json +++ b/frontend/translations/courses/en.json @@ -30,6 +30,8 @@ "courseModules": "Study modules", "coursePhoto": "Photo", "courseNewPhoto": "Upload new photo", + "courseRemovePhoto": "Remove photo", + "coursePhotoPreview": "Photo preview", "courseTranslations": "Course translations", "confirmationAreYouSure": "Are you sure?", "confirmationRemoveTranslation": "Do you want to remove this translation?", diff --git a/frontend/translations/courses/fi.json b/frontend/translations/courses/fi.json index 75f95d27f..50d828bc4 100644 --- a/frontend/translations/courses/fi.json +++ b/frontend/translations/courses/fi.json @@ -30,6 +30,8 @@ "courseModules": "Opintokokonaisuudet", "coursePhoto": "Kuva", "courseNewPhoto": "Lataa uusi kuva", + "courseRemovePhoto": "Poista kuva", + "coursePhotoPreview": "Esikatselu", "courseTranslations": "Käännökset", "confirmationAreYouSure": "Oletko varma?", "confirmationRemoveTranslation": "Haluatko poistaa tämän käännöksen?",