From 39728d4cc4ec7cbce843bcf3ffbad82ebf0c66cd Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 24 Dec 2025 19:30:01 +0530 Subject: [PATCH] [WEB-5779] fix: handle loading state while fetching project cover image (#8419) * refactor: replace cover image handling with CoverImage component across profile and project forms * fix: extend CoverImage component to accept additional img props * Update apps/web/core/components/common/cover-image.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: handle undefined cover image URL in ProfileSidebar component --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../core/components/common/cover-image.tsx | 44 +++++++++++++++++++ apps/web/core/components/profile/form.tsx | 9 ++-- apps/web/core/components/profile/sidebar.tsx | 14 +++--- apps/web/core/components/project/card.tsx | 8 ++-- .../core/components/project/create/header.tsx | 15 +++---- apps/web/core/components/project/form.tsx | 9 ++-- 6 files changed, 68 insertions(+), 31 deletions(-) create mode 100644 apps/web/core/components/common/cover-image.tsx diff --git a/apps/web/core/components/common/cover-image.tsx b/apps/web/core/components/common/cover-image.tsx new file mode 100644 index 0000000000..d0b410bfc3 --- /dev/null +++ b/apps/web/core/components/common/cover-image.tsx @@ -0,0 +1,44 @@ +import { cn } from "@plane/utils"; +// helpers +import { getCoverImageDisplayURL, DEFAULT_COVER_IMAGE_URL } from "@/helpers/cover-image.helper"; + +type TCoverImageProps = { + /** The cover image URL - can be static, uploaded, or external */ + src: string | null | undefined; + /** Alt text for the image */ + alt?: string; + /** Additional className for the image or skeleton */ + className?: string; + /** Whether to show default image when src is null/undefined. If false, shows loading skeleton */ + showDefaultWhenEmpty?: boolean; + /** Custom fallback URL to use instead of DEFAULT_COVER_IMAGE_URL */ + fallbackUrl?: string; +} & React.ComponentProps<"img">; + +/** + * A reusable cover image component that handles: + * - Loading states with skeleton + * - Static images (local assets) + * - Uploaded images (processed through getFileURL) + * - External URLs + * - Fallback to default cover image + */ +export function CoverImage(props: TCoverImageProps) { + const { + src, + alt = "Cover image", + className, + showDefaultWhenEmpty = false, + fallbackUrl = DEFAULT_COVER_IMAGE_URL, + ...restProps + } = props; + + // Show loading skeleton when src is undefined/null and we don't want to show default + if (!src && !showDefaultWhenEmpty) { + return
; + } + + const displayUrl = getCoverImageDisplayURL(src, fallbackUrl); + + return {alt}; +} diff --git a/apps/web/core/components/profile/form.tsx b/apps/web/core/components/profile/form.tsx index bafd3935f7..f39cd7fcc7 100644 --- a/apps/web/core/components/profile/form.tsx +++ b/apps/web/core/components/profile/form.tsx @@ -20,8 +20,9 @@ import { DeactivateAccountModal } from "@/components/account/deactivate-account- import { ImagePickerPopover } from "@/components/core/image-picker-popover"; import { ChangeEmailModal } from "@/components/core/modals/change-email-modal"; import { UserImageUploadModal } from "@/components/core/modals/user-image-upload-modal"; +import { CoverImage } from "@/components/common/cover-image"; // helpers -import { DEFAULT_COVER_IMAGE_URL, getCoverImageDisplayURL, handleCoverImageChange } from "@/helpers/cover-image.helper"; +import { handleCoverImageChange } from "@/helpers/cover-image.helper"; import { captureSuccess, captureError } from "@/helpers/event-tracker.helper"; // hooks import { useInstance } from "@/hooks/store/use-instance"; @@ -210,9 +211,9 @@ export const ProfileForm = observer(function ProfileForm(props: TProfileFormProp
- {currentUser?.first_name
diff --git a/apps/web/core/components/profile/sidebar.tsx b/apps/web/core/components/profile/sidebar.tsx index 5d987cd557..aa332b6313 100644 --- a/apps/web/core/components/profile/sidebar.tsx +++ b/apps/web/core/components/profile/sidebar.tsx @@ -17,8 +17,8 @@ import type { IUserProfileProjectSegregation } from "@plane/types"; // plane ui import { Loader } from "@plane/ui"; import { cn, renderFormattedDate, getFileURL } from "@plane/utils"; -// helpers -import { getCoverImageDisplayURL } from "@/helpers/cover-image.helper"; +// components +import { CoverImage } from "@/components/common/cover-image"; // hooks import { useAppTheme } from "@/hooks/store/use-app-theme"; import { useProject } from "@/hooks/store/use-project"; @@ -101,13 +101,11 @@ export const ProfileSidebar = observer(function ProfileSidebar(props: TProfileSi
)} - {userData?.display_name}
{userData?.avatar_url && userData?.avatar_url !== "" ? ( diff --git a/apps/web/core/components/project/card.tsx b/apps/web/core/components/project/card.tsx index c1fe9b7a33..379bb66268 100644 --- a/apps/web/core/components/project/card.tsx +++ b/apps/web/core/components/project/card.tsx @@ -22,10 +22,10 @@ import { useUserPermissions } from "@/hooks/store/user"; import { useAppRouter } from "@/hooks/use-app-router"; import { usePlatformOS } from "@/hooks/use-platform-os"; // local imports +import { CoverImage } from "@/components/common/cover-image"; import { DeleteProjectModal } from "./delete-project-modal"; import { JoinProjectModal } from "./join-project-modal"; import { ArchiveRestoreProjectModal } from "./settings/archive-project/archive-restore-modal"; -import { DEFAULT_COVER_IMAGE_URL, getCoverImageDisplayURL } from "@/helpers/cover-image.helper"; type Props = { project: IProject; @@ -206,10 +206,10 @@ export const ProjectCard = observer(function ProjectCard(props: Props) {
- {project.name}
diff --git a/apps/web/core/components/project/create/header.tsx b/apps/web/core/components/project/create/header.tsx index 0e3a627b69..b201be4fd0 100644 --- a/apps/web/core/components/project/create/header.tsx +++ b/apps/web/core/components/project/create/header.tsx @@ -10,9 +10,8 @@ import type { IProject } from "@plane/types"; // plane ui import { getTabIndex } from "@plane/utils"; // components +import { CoverImage } from "@/components/common/cover-image"; import { ImagePickerPopover } from "@/components/core/image-picker-popover"; -// helpers -import { DEFAULT_COVER_IMAGE_URL, getCoverImageDisplayURL } from "@/helpers/cover-image.helper"; // plane web imports import { ProjectTemplateSelect } from "@/plane-web/components/projects/create/template-select"; @@ -33,13 +32,11 @@ function ProjectCreateHeader(props: Props) { return (
- {coverImage && ( - {t("project_cover_image_alt")} - )} +
diff --git a/apps/web/core/components/project/form.tsx b/apps/web/core/components/project/form.tsx index 3deb5f66d6..0fd4ad6b51 100644 --- a/apps/web/core/components/project/form.tsx +++ b/apps/web/core/components/project/form.tsx @@ -12,10 +12,11 @@ import { EFileAssetType } from "@plane/types"; import type { IProject, IWorkspace } from "@plane/types"; import { CustomSelect, Input, TextArea } from "@plane/ui"; import { renderFormattedDate } from "@plane/utils"; +import { CoverImage } from "@/components/common/cover-image"; import { ImagePickerPopover } from "@/components/core/image-picker-popover"; import { TimezoneSelect } from "@/components/global"; // helpers -import { DEFAULT_COVER_IMAGE_URL, getCoverImageDisplayURL, handleCoverImageChange } from "@/helpers/cover-image.helper"; +import { handleCoverImageChange } from "@/helpers/cover-image.helper"; import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; // hooks import { useProject } from "@/hooks/store/use-project"; @@ -200,11 +201,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
- Project cover image +