Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
f88b2c8
feat: implement cover image handling and static image selection
JayashTripathy Nov 26, 2025
e667a54
refactor: rename STATIC_COVER_IMAGES_ARRAY to STATIC_COVER_IMAGES for…
JayashTripathy Nov 26, 2025
e10b280
feat: enhance project creation and image handling
JayashTripathy Nov 27, 2025
88f28d3
refactor: simplify cover image type definition and clean up code
JayashTripathy Nov 27, 2025
4d0427d
refactor: update cover image type definitions and simplify logic
JayashTripathy Nov 27, 2025
ba9db3a
refactor: remove unused project cover image endpoint and update cover…
JayashTripathy Nov 27, 2025
f7f8d7a
refactor: update cover image imports to new asset structure
JayashTripathy Nov 27, 2025
d539aa6
feat: add additional cover images to the helper
JayashTripathy Nov 27, 2025
8fee5f2
refactor: remove ProjectPublicCoverImagesEndpoint from project URLs a…
prateekshourya29 Nov 27, 2025
9db8d9d
refactor: update cover image imports to include URL query parameter
JayashTripathy Nov 27, 2025
196e5ff
Merge branch 'feat/app-static-cover-images' of https://github.com/mak…
JayashTripathy Nov 27, 2025
d7b1229
refactor: extract default project form values into a utility function
JayashTripathy Nov 27, 2025
e3661aa
feat: integrate project update functionality in CreateProjectForm
JayashTripathy Nov 28, 2025
bf164b1
fix: update documentation for cover image handling
JayashTripathy Dec 1, 2025
801466e
feat: implement random cover image selection for project forms
JayashTripathy Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions apps/api/plane/app/urls/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
ProjectIdentifierEndpoint,
ProjectFavoritesViewSet,
UserProjectInvitationsViewset,
ProjectPublicCoverImagesEndpoint,
UserProjectRolesEndpoint,
ProjectArchiveUnarchiveEndpoint,
)
Expand Down Expand Up @@ -105,11 +104,6 @@
ProjectFavoritesViewSet.as_view({"delete": "destroy"}),
name="project-favorite",
),
path(
"project-covers/",
ProjectPublicCoverImagesEndpoint.as_view(),
name="project-covers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
DeployBoardViewSet.as_view({"get": "list", "post": "create"}),
Expand Down
1 change: 0 additions & 1 deletion apps/api/plane/app/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
ProjectIdentifierEndpoint,
ProjectUserViewsEndpoint,
ProjectFavoritesViewSet,
ProjectPublicCoverImagesEndpoint,
DeployBoardViewSet,
ProjectArchiveUnarchiveEndpoint,
)
Expand Down
43 changes: 0 additions & 43 deletions apps/api/plane/app/views/project/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,49 +553,6 @@ def destroy(self, request, slug, project_id):
return Response(status=status.HTTP_204_NO_CONTENT)


class ProjectPublicCoverImagesEndpoint(BaseAPIView):
permission_classes = [AllowAny]

# Cache the below api for 24 hours
@cache_response(60 * 60 * 24, user=False)
def get(self, request):
files = []
if settings.USE_MINIO:
s3 = boto3.client(
"s3",
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
else:
s3 = boto3.client(
"s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
params = {
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Prefix": "static/project-cover/",
}

try:
response = s3.list_objects_v2(**params)
# Extracting file keys from the response
if "Contents" in response:
for content in response["Contents"]:
if not content["Key"].endswith(
"/"
): # This line ensures we're only getting files, not "sub-folders"
files.append(
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
)

return Response(files, status=status.HTTP_200_OK)
except Exception as e:
log_exception(e)
return Response([], status=status.HTTP_200_OK)


class DeployBoardViewSet(BaseViewSet):
permission_classes = [ProjectMemberPermission]
serializer_class = DeployBoardSerializer
Expand Down
Binary file added apps/web/app/assets/cover-images/image_1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_10.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_11.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_12.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_13.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_14.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_15.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_16.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_17.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_18.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_19.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_20.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_21.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_22.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_23.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_24.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_25.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_26.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_27.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_28.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_29.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_4.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_5.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/app/assets/cover-images/image_6.jpg
Binary file added apps/web/app/assets/cover-images/image_7.jpg
Binary file added apps/web/app/assets/cover-images/image_8.jpg
Binary file added apps/web/app/assets/cover-images/image_9.jpg
43 changes: 35 additions & 8 deletions apps/web/ce/components/projects/create/root.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import type { FC } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import { FormProvider, useForm } from "react-hook-form";
import { DEFAULT_PROJECT_FORM_VALUES, PROJECT_TRACKER_EVENTS } from "@plane/constants";
import { PROJECT_TRACKER_EVENTS, RANDOM_EMOJI_CODES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EFileAssetType } from "@plane/types";
import type { IProject } from "@plane/types";
// constants
import ProjectCommonAttributes from "@/components/project/create/common-attributes";
import ProjectCreateHeader from "@/components/project/create/header";
import ProjectCreateButtons from "@/components/project/create/project-create-buttons";
// hooks
import { DEFAULT_COVER_IMAGE_URL, getCoverImageType, uploadCoverImage } from "@/helpers/cover-image.helper";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
import { useProject } from "@/hooks/store/use-project";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web types
import type { TProject } from "@/plane-web/types/projects";
import ProjectAttributes from "./attributes";
import { getProjectFormValues } from "./utils";

export type TCreateProjectFormProps = {
setToFavorite?: boolean;
Expand All @@ -37,7 +40,7 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
// form info
const methods = useForm<TProject>({
defaultValues: { ...DEFAULT_PROJECT_FORM_VALUES, ...data },
defaultValues: { ...getProjectFormValues(), ...data },
reValidateMode: "onChange",
});
const { handleSubmit, reset, setValue } = methods;
Expand All @@ -58,15 +61,39 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
// Upper case identifier
formData.identifier = formData.identifier?.toUpperCase();
const coverImage = formData.cover_image_url;
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
if (coverImage?.startsWith("http")) {
formData.cover_image = coverImage;
formData.cover_image_asset = null;
let uploadedAssetUrl: string | null = null;

if (coverImage) {
const imageType = getCoverImageType(coverImage);

if (imageType === "local_static") {
try {
uploadedAssetUrl = await uploadCoverImage(coverImage, {
workspaceSlug: workspaceSlug.toString(),
entityIdentifier: "",
entityType: EFileAssetType.PROJECT_COVER,
isUserAsset: false,
});
} catch (error) {
console.error("Error uploading cover image:", error);
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: error instanceof Error ? error.message : "Failed to upload cover image",
});
return Promise.reject(error);
}
} else {
formData.cover_image = coverImage;
formData.cover_image_asset = null;
}
}

return createProject(workspaceSlug.toString(), formData)
.then(async (res) => {
if (coverImage) {
if (uploadedAssetUrl) {
await updateCoverImageStatus(res.id, uploadedAssetUrl);
} else if (coverImage && coverImage.startsWith("http")) {
await updateCoverImageStatus(res.id, coverImage);
}
captureSuccess({
Expand Down
18 changes: 18 additions & 0 deletions apps/web/ce/components/projects/create/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { RANDOM_EMOJI_CODES } from "@plane/constants";
import type { IProject } from "@plane/types";
import { DEFAULT_COVER_IMAGE_URL } from "@/helpers/cover-image.helper";

export const getProjectFormValues = (): Partial<IProject> => ({
cover_image_url: DEFAULT_COVER_IMAGE_URL,
description: "",
logo_props: {
in_use: "emoji",
emoji: {
value: RANDOM_EMOJI_CODES[Math.floor(Math.random() * RANDOM_EMOJI_CODES.length)],
},
},
identifier: "",
name: "",
network: 2,
project_lead: null,
});
Loading
Loading