mirror of
https://github.com/makeplane/plane.git
synced 2026-08-29 10:08:51 +02:00
Updated the `updateCurrentUser` method in `UserStore` to clone the current user data before making updates, ensuring that the original data remains unchanged during the update process. Additionally, added logic to update the local state with the new user data after a successful update. fix(cover-image): return absolute URLs for cover images Modified the `handleCoverImageChange` function to return absolute URLs for cover images, ensuring compatibility with the expected format. This change includes handling both uploaded images and new images, providing a consistent return structure.
290 lines
9.1 KiB
TypeScript
290 lines
9.1 KiB
TypeScript
/**
|
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
* See the LICENSE file for details.
|
|
*/
|
|
|
|
import type { EFileAssetType } from "@plane/types";
|
|
import { getFileURL } from "@plane/utils";
|
|
|
|
import CoverImage1 from "@/app/assets/cover-images/image_1.jpg?url";
|
|
import CoverImage10 from "@/app/assets/cover-images/image_10.jpg?url";
|
|
import CoverImage11 from "@/app/assets/cover-images/image_11.jpg?url";
|
|
import CoverImage12 from "@/app/assets/cover-images/image_12.jpg?url";
|
|
import CoverImage13 from "@/app/assets/cover-images/image_13.jpg?url";
|
|
import CoverImage14 from "@/app/assets/cover-images/image_14.jpg?url";
|
|
import CoverImage15 from "@/app/assets/cover-images/image_15.jpg?url";
|
|
import CoverImage16 from "@/app/assets/cover-images/image_16.jpg?url";
|
|
import CoverImage17 from "@/app/assets/cover-images/image_17.jpg?url";
|
|
import CoverImage18 from "@/app/assets/cover-images/image_18.jpg?url";
|
|
import CoverImage19 from "@/app/assets/cover-images/image_19.jpg?url";
|
|
import CoverImage2 from "@/app/assets/cover-images/image_2.jpg?url";
|
|
import CoverImage20 from "@/app/assets/cover-images/image_20.jpg?url";
|
|
import CoverImage21 from "@/app/assets/cover-images/image_21.jpg?url";
|
|
import CoverImage22 from "@/app/assets/cover-images/image_22.jpg?url";
|
|
import CoverImage23 from "@/app/assets/cover-images/image_23.jpg?url";
|
|
import CoverImage24 from "@/app/assets/cover-images/image_24.jpg?url";
|
|
import CoverImage25 from "@/app/assets/cover-images/image_25.jpg?url";
|
|
import CoverImage26 from "@/app/assets/cover-images/image_26.jpg?url";
|
|
import CoverImage27 from "@/app/assets/cover-images/image_27.jpg?url";
|
|
import CoverImage28 from "@/app/assets/cover-images/image_28.jpg?url";
|
|
import CoverImage29 from "@/app/assets/cover-images/image_29.jpg?url";
|
|
import CoverImage3 from "@/app/assets/cover-images/image_3.jpg?url";
|
|
import CoverImage4 from "@/app/assets/cover-images/image_4.jpg?url";
|
|
import CoverImage5 from "@/app/assets/cover-images/image_5.jpg?url";
|
|
import CoverImage6 from "@/app/assets/cover-images/image_6.jpg?url";
|
|
import CoverImage7 from "@/app/assets/cover-images/image_7.jpg?url";
|
|
import CoverImage8 from "@/app/assets/cover-images/image_8.jpg?url";
|
|
import CoverImage9 from "@/app/assets/cover-images/image_9.jpg?url";
|
|
|
|
import { FileService } from "@/services/file.service";
|
|
|
|
const fileService = new FileService();
|
|
|
|
/**
|
|
* Map of all available static cover images
|
|
* These are pre-loaded images available in the assets/cover-images folder
|
|
*/
|
|
export const STATIC_COVER_IMAGES = {
|
|
IMAGE_1: CoverImage1,
|
|
IMAGE_2: CoverImage2,
|
|
IMAGE_3: CoverImage3,
|
|
IMAGE_4: CoverImage4,
|
|
IMAGE_5: CoverImage5,
|
|
IMAGE_6: CoverImage6,
|
|
IMAGE_7: CoverImage7,
|
|
IMAGE_8: CoverImage8,
|
|
IMAGE_9: CoverImage9,
|
|
IMAGE_10: CoverImage10,
|
|
IMAGE_11: CoverImage11,
|
|
IMAGE_12: CoverImage12,
|
|
IMAGE_13: CoverImage13,
|
|
IMAGE_14: CoverImage14,
|
|
IMAGE_15: CoverImage15,
|
|
IMAGE_16: CoverImage16,
|
|
IMAGE_17: CoverImage17,
|
|
IMAGE_18: CoverImage18,
|
|
IMAGE_19: CoverImage19,
|
|
IMAGE_20: CoverImage20,
|
|
IMAGE_21: CoverImage21,
|
|
IMAGE_22: CoverImage22,
|
|
IMAGE_23: CoverImage23,
|
|
IMAGE_24: CoverImage24,
|
|
IMAGE_25: CoverImage25,
|
|
IMAGE_26: CoverImage26,
|
|
IMAGE_27: CoverImage27,
|
|
IMAGE_28: CoverImage28,
|
|
IMAGE_29: CoverImage29,
|
|
} as const;
|
|
|
|
export const DEFAULT_COVER_IMAGE_URL = STATIC_COVER_IMAGES.IMAGE_1;
|
|
|
|
/**
|
|
* Set of static image URLs for fast O(1) lookup
|
|
*/
|
|
const STATIC_COVER_IMAGES_SET = new Set<string>(Object.values(STATIC_COVER_IMAGES));
|
|
|
|
export type TCoverImageType = "local_static" | "uploaded_asset" | "unsplash";
|
|
|
|
export type TCoverImageResult = {
|
|
needsUpload: boolean;
|
|
imageType: TCoverImageType;
|
|
shouldUpdate: boolean;
|
|
};
|
|
|
|
export type TCoverImagePayload = {
|
|
cover_image?: string | null;
|
|
cover_image_url?: string | null;
|
|
cover_image_asset?: string | null;
|
|
};
|
|
|
|
/**
|
|
* Checks if a given URL is a valid static cover image
|
|
*/
|
|
export const isStaticCoverImage = (imageUrl: string | null | undefined): boolean => {
|
|
if (!imageUrl) return false;
|
|
return STATIC_COVER_IMAGES_SET.has(imageUrl);
|
|
};
|
|
|
|
/**
|
|
* Determines the type of cover image URL
|
|
* Uses explicit validation against known static images for better accuracy
|
|
*/
|
|
export const getCoverImageType = (imageUrl: string): TCoverImageType => {
|
|
// Check against the explicit set of static images
|
|
if (isStaticCoverImage(imageUrl)) return "local_static";
|
|
|
|
// Check if it's an Unsplash image by validating the hostname
|
|
try {
|
|
const url = new URL(imageUrl);
|
|
const hostname = url.hostname.toLowerCase();
|
|
if (hostname === "unsplash.com" || hostname.endsWith(".unsplash.com")) {
|
|
return "unsplash";
|
|
}
|
|
} catch {
|
|
// If URL parsing fails (e.g., relative path), fall through to other checks
|
|
}
|
|
|
|
if (imageUrl.startsWith("http")) return "uploaded_asset";
|
|
|
|
return "uploaded_asset";
|
|
};
|
|
|
|
/**
|
|
* Gets the correct display URL for a cover image
|
|
* - Local static images: returned as-is (served from assets folder)
|
|
* - Uploaded assets: processed through getFileURL (adds backend URL)
|
|
*/
|
|
export function getCoverImageDisplayURL(imageUrl: string | null | undefined, fallbackUrl: string): string;
|
|
export function getCoverImageDisplayURL(imageUrl: string | null | undefined, fallbackUrl: null): string | null;
|
|
export function getCoverImageDisplayURL(
|
|
imageUrl: string | null | undefined,
|
|
fallbackUrl: string | null
|
|
): string | null {
|
|
if (!imageUrl) {
|
|
return fallbackUrl;
|
|
}
|
|
|
|
const imageType = getCoverImageType(imageUrl);
|
|
|
|
if (imageType === "local_static" || imageType === "unsplash") {
|
|
return imageUrl;
|
|
}
|
|
|
|
if (imageType === "uploaded_asset") {
|
|
return getFileURL(imageUrl) || imageUrl;
|
|
}
|
|
|
|
return imageUrl;
|
|
}
|
|
|
|
/**
|
|
* Analyzes cover image change and determines what action to take
|
|
* Merged with isUnsplashImage logic - now detects unsplash images as a separate type
|
|
*/
|
|
export const analyzeCoverImageChange = (
|
|
currentImage: string | null | undefined,
|
|
newImage: string | null | undefined
|
|
): TCoverImageResult => {
|
|
const hasChanged = currentImage !== newImage;
|
|
|
|
if (!hasChanged) {
|
|
return {
|
|
needsUpload: false,
|
|
imageType: "uploaded_asset",
|
|
shouldUpdate: false,
|
|
};
|
|
}
|
|
|
|
if (!newImage) {
|
|
return {
|
|
needsUpload: false,
|
|
imageType: "uploaded_asset",
|
|
shouldUpdate: true,
|
|
};
|
|
}
|
|
|
|
const imageType = getCoverImageType(newImage);
|
|
|
|
return {
|
|
needsUpload: imageType === "local_static" || imageType === "unsplash",
|
|
imageType,
|
|
shouldUpdate: hasChanged,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Uploads a local static image to S3
|
|
*/
|
|
export const uploadCoverImage = async (
|
|
imageUrl: string,
|
|
uploadConfig: {
|
|
workspaceSlug?: string;
|
|
entityIdentifier: string;
|
|
entityType: EFileAssetType;
|
|
isUserAsset?: boolean;
|
|
}
|
|
): Promise<string> => {
|
|
const { workspaceSlug, entityIdentifier, entityType, isUserAsset = false } = uploadConfig;
|
|
|
|
// Fetch the local image
|
|
const response = await fetch(imageUrl);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch image: ${response.statusText}`);
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
|
|
// Validate it's actually an image
|
|
if (!blob.type.startsWith("image/")) {
|
|
throw new Error("Invalid file type. Please select an image.");
|
|
}
|
|
|
|
const fileName = imageUrl.split("/").pop()?.split("?")[0] || "image.jpg";
|
|
const file = new File([blob], fileName, { type: blob.type });
|
|
|
|
// Upload based on context
|
|
if (isUserAsset) {
|
|
const uploadResult = await fileService.uploadUserAsset(
|
|
{
|
|
entity_identifier: entityIdentifier,
|
|
entity_type: entityType,
|
|
},
|
|
file
|
|
);
|
|
return uploadResult.asset_url;
|
|
} else {
|
|
if (!workspaceSlug) {
|
|
throw new Error("Workspace slug is required for workspace asset upload");
|
|
}
|
|
|
|
const uploadResult = await fileService.uploadWorkspaceAsset(
|
|
workspaceSlug,
|
|
{
|
|
entity_identifier: entityIdentifier,
|
|
entity_type: entityType,
|
|
},
|
|
file
|
|
);
|
|
return uploadResult.asset_url;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Main utility to handle cover image changes with upload
|
|
*/
|
|
export const handleCoverImageChange = async (
|
|
currentImage: string | null | undefined,
|
|
newImage: string | null | undefined,
|
|
uploadConfig: {
|
|
workspaceSlug?: string;
|
|
entityIdentifier: string;
|
|
entityType: EFileAssetType;
|
|
isUserAsset?: boolean;
|
|
}
|
|
): Promise<TCoverImagePayload | undefined> => {
|
|
const analysis = analyzeCoverImageChange(currentImage, newImage);
|
|
if (!analysis.shouldUpdate) return;
|
|
|
|
if (!newImage) {
|
|
return { cover_image: null, cover_image_url: null, cover_image_asset: null };
|
|
}
|
|
|
|
if (analysis.needsUpload) {
|
|
const assetUrl = await uploadCoverImage(newImage, uploadConfig);
|
|
// cover_image requires an absolute URL; cover_image_url is relative (matches GET /api/users/me/ format)
|
|
return { cover_image: getFileURL(assetUrl) || assetUrl, cover_image_url: assetUrl };
|
|
}
|
|
|
|
// cover_image requires an absolute URL; getFileURL converts relative paths from the Upload tab
|
|
return { cover_image: getFileURL(newImage) || newImage, cover_image_url: newImage };
|
|
};
|
|
|
|
/**
|
|
* Returns a random cover image from the STATIC_COVER_IMAGES object
|
|
* @returns {string} A random cover image URL
|
|
*/
|
|
export const getRandomCoverImage = (): string =>
|
|
Object.values(STATIC_COVER_IMAGES)[Math.floor(Math.random() * Object.keys(STATIC_COVER_IMAGES).length)];
|