diff --git a/apiserver/plane/app/views/asset/v2.py b/apiserver/plane/app/views/asset/v2.py index aa777fff700..e17b5652929 100644 --- a/apiserver/plane/app/views/asset/v2.py +++ b/apiserver/plane/app/views/asset/v2.py @@ -235,7 +235,10 @@ def get_entity_id_field(self, entity_type, entity_id): } # Project Cover - if entity_type == FileAsset.EntityTypeContext.PROJECT_COVER: + if ( + entity_type == FileAsset.EntityTypeContext.PROJECT_COVER + or entity_type == FileAsset.EntityTypeContext.PROJECT_LOGO + ): return { "project_id": entity_id, } @@ -522,6 +525,7 @@ def get(self, request, asset_id): FileAsset.EntityTypeContext.USER_COVER, FileAsset.EntityTypeContext.WORKSPACE_LOGO, FileAsset.EntityTypeContext.PROJECT_COVER, + FileAsset.EntityTypeContext.PROJECT_LOGO, ]: return Response( { @@ -562,7 +566,10 @@ def get_entity_id_field(self, entity_type, entity_id): "workspace_id": entity_id, } - if entity_type == FileAsset.EntityTypeContext.PROJECT_COVER: + if ( + entity_type == FileAsset.EntityTypeContext.PROJECT_COVER + or entity_type == FileAsset.EntityTypeContext.PROJECT_LOGO + ): return { "project_id": entity_id, } @@ -737,7 +744,6 @@ def get(self, request, slug, project_id, pk): class ProjectBulkAssetEndpoint(BaseAPIView): - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def post(self, request, slug, project_id, entity_id): asset_ids = request.data.get("asset_ids", []) @@ -769,11 +775,21 @@ def post(self, request, slug, project_id, entity_id): ) # Check if the asset is uploaded - if asset.entity_type == FileAsset.EntityTypeContext.PROJECT_COVER: + if ( + asset.entity_type == FileAsset.EntityTypeContext.PROJECT_COVER + or asset.entity_type == FileAsset.EntityTypeContext.PROJECT_LOGO + ): + # Update the project cover image assets.update( project_id=project_id, ) + # Update the project cover image + if asset.entity_type == FileAsset.EntityTypeContext.PROJECT_COVER: + project = Project.objects.get(id=project_id) + project.cover_image_asset_id = asset.id + project.save() + if asset.entity_type == FileAsset.EntityTypeContext.ISSUE_DESCRIPTION: assets.update( issue_id=entity_id, diff --git a/apiserver/plane/db/migrations/0083_alter_fileasset_entity_type.py b/apiserver/plane/db/migrations/0083_alter_fileasset_entity_type.py new file mode 100644 index 00000000000..72c6a7fe4cd --- /dev/null +++ b/apiserver/plane/db/migrations/0083_alter_fileasset_entity_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.15 on 2024-10-24 19:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0082_alter_issue_managers_alter_cycleissue_issue_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='fileasset', + name='entity_type', + field=models.CharField(blank=True, choices=[('ISSUE_ATTACHMENT', 'Issue Attachment'), ('ISSUE_DESCRIPTION', 'Issue Description'), ('COMMENT_DESCRIPTION', 'Comment Description'), ('PAGE_DESCRIPTION', 'Page Description'), ('USER_COVER', 'User Cover'), ('USER_AVATAR', 'User Avatar'), ('WORKSPACE_LOGO', 'Workspace Logo'), ('PROJECT_COVER', 'Project Cover'), ('DRAFT_ISSUE_ATTACHMENT', 'Draft Issue Attachment'), ('DRAFT_ISSUE_DESCRIPTION', 'Draft Issue Description'), ('PROJECT_LOGO', 'Project Logo')], max_length=255, null=True), + ), + ] diff --git a/apiserver/plane/db/models/asset.py b/apiserver/plane/db/models/asset.py index e230d3aecf6..7ec83c2f2df 100644 --- a/apiserver/plane/db/models/asset.py +++ b/apiserver/plane/db/models/asset.py @@ -11,7 +11,6 @@ def get_upload_path(instance, filename): - if instance.workspace_id is not None: return f"{instance.workspace.id}/{uuid4().hex}-{filename}" return f"user-{uuid4().hex}-{filename}" @@ -38,6 +37,7 @@ class EntityTypeContext(models.TextChoices): PROJECT_COVER = "PROJECT_COVER" DRAFT_ISSUE_ATTACHMENT = "DRAFT_ISSUE_ATTACHMENT" DRAFT_ISSUE_DESCRIPTION = "DRAFT_ISSUE_DESCRIPTION" + PROJECT_LOGO = "PROJECT_LOGO" attributes = models.JSONField(default=dict) asset = models.FileField( @@ -116,6 +116,7 @@ def asset_url(self): or self.entity_type == self.EntityTypeContext.USER_AVATAR or self.entity_type == self.EntityTypeContext.USER_COVER or self.entity_type == self.EntityTypeContext.PROJECT_COVER + or self.entity_type == self.EntityTypeContext.PROJECT_LOGO ): return f"/api/assets/v2/static/{self.id}/" diff --git a/packages/types/src/common.d.ts b/packages/types/src/common.d.ts index 5fe31ad0006..137600a5d10 100644 --- a/packages/types/src/common.d.ts +++ b/packages/types/src/common.d.ts @@ -11,7 +11,7 @@ export type TPaginationInfo = { }; export type TLogoProps = { - in_use: "emoji" | "icon"; + in_use: "emoji" | "icon" | "image"; emoji?: { value?: string; url?: string; @@ -21,4 +21,7 @@ export type TLogoProps = { color?: string; background_color?: string; }; + image?: { + url?: string; + } }; diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts index df6a462b02e..e4d31f210e5 100644 --- a/packages/types/src/enums.ts +++ b/packages/types/src/enums.ts @@ -56,6 +56,7 @@ export enum EFileAssetType { DRAFT_ISSUE_DESCRIPTION = "DRAFT_ISSUE_DESCRIPTION", PAGE_DESCRIPTION = "PAGE_DESCRIPTION", PROJECT_COVER = "PROJECT_COVER", + PROJECT_LOGO = "PROJECT_LOGO", USER_AVATAR = "USER_AVATAR", USER_COVER = "USER_COVER", WORKSPACE_LOGO = "WORKSPACE_LOGO", diff --git a/packages/ui/package.json b/packages/ui/package.json index 9aa1b087241..136f75a5a01 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -30,13 +30,14 @@ "@blueprintjs/core": "^4.16.3", "@blueprintjs/popover2": "^1.13.3", "@headlessui/react": "^1.7.3", - "@popperjs/core": "^2.11.8", "@plane/helpers": "*", + "@popperjs/core": "^2.11.8", "clsx": "^2.0.0", "emoji-picker-react": "^4.5.16", "lodash": "^4.17.21", "lucide-react": "^0.379.0", "react-color": "^2.19.3", + "react-dropzone": "^14.2.10", "react-popper": "^2.3.0", "sonner": "^1.4.41", "tailwind-merge": "^2.0.0", @@ -44,6 +45,8 @@ }, "devDependencies": { "@chromatic-com/storybook": "^1.4.0", + "@plane/eslint-config": "*", + "@plane/typescript-config": "*", "@storybook/addon-essentials": "^8.1.1", "@storybook/addon-interactions": "^8.1.1", "@storybook/addon-links": "^8.1.1", @@ -61,13 +64,11 @@ "@types/react-dom": "^18.2.18", "autoprefixer": "^10.4.19", "classnames": "^2.3.2", - "@plane/eslint-config": "*", "postcss-cli": "^11.0.0", "postcss-nested": "^6.0.1", "storybook": "^8.1.1", "tailwind-config-custom": "*", "tailwindcss": "^3.4.3", - "@plane/typescript-config": "*", "tsup": "^7.2.0", "typescript": "5.3.3" } diff --git a/packages/ui/src/emoji/custom-image-picker.tsx b/packages/ui/src/emoji/custom-image-picker.tsx new file mode 100644 index 00000000000..6def2903773 --- /dev/null +++ b/packages/ui/src/emoji/custom-image-picker.tsx @@ -0,0 +1,88 @@ +import React, { useCallback, useState } from "react"; +import { Button } from "../button"; +import { Input } from "../form-fields"; +import Dropzone from "react-dropzone"; +import { cn } from "../../helpers"; + +type Props = { + onChange: (url: string) => void; + uploadFile: (file: File) => Promise; +}; + +export const ProjectLogoCustomImagePicker: React.FC = (props) => { + const { onChange, uploadFile } = props; + // states + const [externalURL, setExternalURL] = useState(""); + + const handleFormSubmit = useCallback(() => { + if (!externalURL || externalURL.trim() === "") return; + onChange(externalURL); + }, [externalURL]); + + const handleUpload = useCallback((files: File[]) => { + if (files.length > 1) return; + const acceptedFile = files[0]; + if (!acceptedFile) return; + uploadFile(acceptedFile).then((url) => { + onChange(url); + }); + }, []); + + return ( + <> +
+ setExternalURL(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleFormSubmit(); + }} + required + autoFocus + /> + +
+ + {({ getRootProps, getInputProps, isDragActive, fileRejections }) => ( +
0, + } + )} + {...getRootProps()} + > + + {fileRejections.length > 0 ? ( +

+ {fileRejections[0].errors[0].code === "file-too-large" + ? "The image size cannot exceed 5 MB." + : "Please upload a valid file."} +

+ ) : ( +

+ Drag 'n' drop some files here, +
+ or click to select files +

+ )} +
+ )} +
+ + ); +}; diff --git a/packages/ui/src/emoji/emoji-icon-helper.tsx b/packages/ui/src/emoji/emoji-icon-helper.tsx index 4643dcb939e..d54eea18eaf 100644 --- a/packages/ui/src/emoji/emoji-icon-helper.tsx +++ b/packages/ui/src/emoji/emoji-icon-helper.tsx @@ -4,8 +4,24 @@ import { EmojiClickData, Theme } from "emoji-picker-react"; export enum EmojiIconPickerTypes { EMOJI = "emoji", ICON = "icon", + IMAGE = "image", } +export const PROJECT_LOGO_PICKER_TABS_LIST = [ + { + key: EmojiIconPickerTypes.EMOJI, + title: "Emojis", + }, + { + key: EmojiIconPickerTypes.ICON, + title: "Icons", + }, + { + key: EmojiIconPickerTypes.IMAGE, + title: "Custom", + }, +]; + export const TABS_LIST = [ { key: EmojiIconPickerTypes.EMOJI, @@ -28,6 +44,10 @@ export type TChangeHandlerProps = name: string; color: string; }; + } + | { + type: EmojiIconPickerTypes.IMAGE; + value: string; }; export type TCustomEmojiPicker = { diff --git a/packages/ui/src/emoji/index.ts b/packages/ui/src/emoji/index.ts index c881d88972c..26acd74edea 100644 --- a/packages/ui/src/emoji/index.ts +++ b/packages/ui/src/emoji/index.ts @@ -1,5 +1,5 @@ export * from "./emoji-icon-picker-new"; -export * from "./emoji-icon-picker"; export * from "./emoji-icon-helper"; export * from "./icons"; export * from "./logo"; +export * from "./project-logo-picker"; diff --git a/packages/ui/src/emoji/emoji-icon-picker.tsx b/packages/ui/src/emoji/project-logo-picker.tsx similarity index 80% rename from packages/ui/src/emoji/emoji-icon-picker.tsx rename to packages/ui/src/emoji/project-logo-picker.tsx index 678a322439b..553d41ab0e0 100644 --- a/packages/ui/src/emoji/emoji-icon-picker.tsx +++ b/packages/ui/src/emoji/project-logo-picker.tsx @@ -9,9 +9,14 @@ import { IconsList } from "./icons-list"; // helpers import { cn } from "../../helpers"; // hooks -import { EmojiIconPickerTypes, TABS_LIST, TCustomEmojiPicker } from "./emoji-icon-helper"; +import { EmojiIconPickerTypes, PROJECT_LOGO_PICKER_TABS_LIST, TCustomEmojiPicker } from "./emoji-icon-helper"; +import { ProjectLogoCustomImagePicker } from "./custom-image-picker"; -export const CustomEmojiIconPicker: React.FC = (props) => { +type Props = TCustomEmojiPicker & { + uploadFile: (file: File) => Promise; +}; + +export const ProjectLogoPickerDropdown: React.FC = (props) => { const { isOpen, handleToggle, @@ -28,6 +33,7 @@ export const CustomEmojiIconPicker: React.FC = (props) => { searchDisabled = false, searchPlaceholder = "Search", theme, + uploadFile, } = props; // refs const containerRef = useRef(null); @@ -78,10 +84,10 @@ export const CustomEmojiIconPicker: React.FC = (props) => { ref={containerRef} as="div" className="h-full w-full flex flex-col overflow-hidden" - defaultIndex={TABS_LIST.findIndex((tab) => tab.key === defaultOpen)} + defaultIndex={PROJECT_LOGO_PICKER_TABS_LIST.findIndex((tab) => tab.key === defaultOpen)} > - - {TABS_LIST.map((tab) => ( + + {PROJECT_LOGO_PICKER_TABS_LIST.map((tab) => ( @@ -128,6 +134,18 @@ export const CustomEmojiIconPicker: React.FC = (props) => { searchDisabled={searchDisabled} /> + + { + onChange({ + type: EmojiIconPickerTypes.IMAGE, + value: url, + }); + if (closeOnSelect) handleToggle(false); + }} + /> + diff --git a/web/ce/components/projects/create/root.tsx b/web/ce/components/projects/create/root.tsx index 04c6dfc882f..a514edafa2c 100644 --- a/web/ce/components/projects/create/root.tsx +++ b/web/ce/components/projects/create/root.tsx @@ -26,7 +26,7 @@ export type TCreateProjectFormProps = { onClose: () => void; handleNextStep: (projectId: string) => void; data?: Partial; - updateCoverImageStatus: (projectId: string, coverImage: string) => Promise; + updateProjectAssetsStatus: (args: { projectId: string; formData: Partial }) => Promise; }; const defaultValues: Partial = { @@ -45,7 +45,7 @@ const defaultValues: Partial = { }; export const CreateProjectForm: FC = observer((props) => { - const { setToFavorite, workspaceSlug, onClose, handleNextStep, updateCoverImageStatus } = props; + const { setToFavorite, workspaceSlug, onClose, handleNextStep, updateProjectAssetsStatus } = props; // store const { captureProjectEvent } = useEventTracker(); const { addProjectToFavorites, createProject } = useProject(); @@ -73,13 +73,13 @@ export const CreateProjectForm: FC = observer((props) = const onSubmit = async (formData: Partial) => { // Upper case identifier formData.identifier = formData.identifier?.toUpperCase(); - const coverImage = formData.cover_image_url; return createProject(workspaceSlug.toString(), formData) .then(async (res) => { - if (coverImage) { - await updateCoverImageStatus(res.id, coverImage); - } + await updateProjectAssetsStatus({ + projectId: res.id, + formData, + }); const newPayload = { ...res, state: "SUCCESS", diff --git a/web/core/components/analytics/custom-analytics/sidebar/projects-list.tsx b/web/core/components/analytics/custom-analytics/sidebar/projects-list.tsx index d7080746763..7ac460ec5bf 100644 --- a/web/core/components/analytics/custom-analytics/sidebar/projects-list.tsx +++ b/web/core/components/analytics/custom-analytics/sidebar/projects-list.tsx @@ -29,8 +29,8 @@ export const CustomAnalyticsSidebarProjectsList: React.FC = observer((pro return (
-
- +
+

{truncateText(project.name, 20)}

diff --git a/web/core/components/common/logo.tsx b/web/core/components/common/logo.tsx index 02b26a10be9..073e731f751 100644 --- a/web/core/components/common/logo.tsx +++ b/web/core/components/common/logo.tsx @@ -1,29 +1,32 @@ "use client"; import { FC } from "react"; -// emoji-picker-react import { Emoji } from "emoji-picker-react"; +import useFontFaceObserver from "use-font-face-observer"; +// plane types import { TLogoProps } from "@plane/types"; -// helpers +// plane ui import { LUCIDE_ICONS_LIST } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; import { emojiCodeToUnicode } from "@/helpers/emoji.helper"; -// import { icons } from "lucide-react"; -import useFontFaceObserver from "use-font-face-observer"; +import { getFileURL } from "@/helpers/file.helper"; type Props = { + imageClassName?: string; logo: TLogoProps; size?: number; type?: "lucide" | "material"; }; export const Logo: FC = (props) => { - const { logo, size = 16, type = "material" } = props; + const { imageClassName, logo, size = 16, type = "material" } = props; // destructuring the logo object - const { in_use, emoji, icon } = logo; + const { in_use, emoji, icon, image } = logo; // derived values - const value = in_use === "emoji" ? emoji?.value : icon?.name; + const value = in_use === "emoji" ? emoji?.value : in_use === "icon" ? icon?.name : image?.url; const color = icon?.color; const lucideIcon = LUCIDE_ICONS_LIST.find((item) => item.name === value); @@ -87,6 +90,14 @@ export const Logo: FC = (props) => { ); } + if (in_use === "image") { + return ( + + Project logo + + ); + } + // if no value, return empty fragment return <>; }; diff --git a/web/core/components/dashboard/widgets/recent-projects.tsx b/web/core/components/dashboard/widgets/recent-projects.tsx index 5255908717e..e6194c74888 100644 --- a/web/core/components/dashboard/widgets/recent-projects.tsx +++ b/web/core/components/dashboard/widgets/recent-projects.tsx @@ -39,12 +39,8 @@ const ProjectListItem: React.FC = observer((props) => { return ( -
-
- -
+
+
diff --git a/web/core/components/profile/sidebar.tsx b/web/core/components/profile/sidebar.tsx index 7990f9b3d0e..99b8f5c80c7 100644 --- a/web/core/components/profile/sidebar.tsx +++ b/web/core/components/profile/sidebar.tsx @@ -157,7 +157,7 @@ export const ProfileSidebar: FC = observer((props) => {
- +
{projectDetails.name}
diff --git a/web/core/components/project/create-project-modal.tsx b/web/core/components/project/create-project-modal.tsx index d0015e32a19..5b67f2ef215 100644 --- a/web/core/components/project/create-project-modal.tsx +++ b/web/core/components/project/create-project-modal.tsx @@ -3,7 +3,6 @@ import { useEffect, FC, useState } from "react"; import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; // helpers import { getAssetIdFromUrl } from "@/helpers/file.helper"; -import { checkURLValidity } from "@/helpers/string.helper"; // plane web components import { CreateProjectForm } from "@/plane-web/components/projects/create/root"; // plane web types @@ -45,10 +44,20 @@ export const CreateProjectModal: FC = (props) => { setCurrentStep(EProjectCreationSteps.FEATURE_SELECTION); }; - const handleCoverImageStatusUpdate = async (projectId: string, coverImage: string) => { - if (!checkURLValidity(coverImage)) { + const handleProjectAssetsStatusUpdate = async (args: { projectId: string; formData: Partial }) => { + const { projectId, formData } = args; + const { cover_image_url: coverImage, logo_props } = formData; + const projectLogo = logo_props?.image?.url; + const assetIds: string[] = []; + if (coverImage && !coverImage.startsWith("http")) { + assetIds.push(getAssetIdFromUrl(coverImage)); + } + if (projectLogo && !projectLogo.startsWith("http")) { + assetIds.push(getAssetIdFromUrl(projectLogo)); + } + if (assetIds.length > 0) { await fileService.updateBulkProjectAssetsUploadStatus(workspaceSlug, projectId, projectId, { - asset_ids: [getAssetIdFromUrl(coverImage)], + asset_ids: assetIds, }); } }; @@ -60,7 +69,7 @@ export const CreateProjectModal: FC = (props) => { setToFavorite={setToFavorite} workspaceSlug={workspaceSlug} onClose={onClose} - updateCoverImageStatus={handleCoverImageStatusUpdate} + updateProjectAssetsStatus={handleProjectAssetsStatusUpdate} handleNextStep={handleNextStep} data={data} /> diff --git a/web/core/components/project/create/header.tsx b/web/core/components/project/create/header.tsx index 6bdbb3c7420..f2e36f29646 100644 --- a/web/core/components/project/create/header.tsx +++ b/web/core/components/project/create/header.tsx @@ -1,16 +1,15 @@ import { useState } from "react"; +import { useParams } from "next/navigation"; import { Controller, useFormContext } from "react-hook-form"; import { X } from "lucide-react"; // plane types import { IProject } from "@plane/types"; -// plane ui -import { CustomEmojiIconPicker, EmojiIconPickerTypes, Logo } from "@plane/ui"; // components import { ImagePickerPopover } from "@/components/core"; +import { ProjectLogoPicker } from "@/components/project"; // constants import { ETabIndices } from "@/constants/tab-indices"; // helpers -import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; import { getFileURL } from "@/helpers/file.helper"; import { getTabIndex } from "@/helpers/tab-indices.helper"; @@ -20,11 +19,15 @@ type Props = { }; const ProjectCreateHeader: React.FC = (props) => { const { handleClose, isMobile = false } = props; + // states + const [isOpen, setIsOpen] = useState(false); + // params + const { workspaceSlug } = useParams(); + // form const { watch, control } = useFormContext(); // derived values const coverImage = watch("cover_image_url"); - const [isOpen, setIsOpen] = useState(false); const { getIndex } = getTabIndex(ETabIndices.PROJECT_CREATE, isMobile); return ( @@ -62,36 +65,16 @@ const ProjectCreateHeader: React.FC = (props) => { name="logo_props" control={control} render={({ field: { value, onChange } }) => ( - setIsOpen(val)} isOpen={isOpen} - handleToggle={(val: boolean) => setIsOpen(val)} - className="flex items-center justify-center" - buttonClassName="flex items-center justify-center" - label={ - - - - } - onChange={(val: any) => { - let logoValue = {}; - - if (val?.type === "emoji") - logoValue = { - value: convertHexEmojiToDecimal(val.value.unified), - url: val.value.imageUrl, - }; - else if (val?.type === "icon") logoValue = val.value; - - onChange({ - in_use: val?.type, - [val?.type]: logoValue, - }); + onChange={(val) => { + onChange(val); setIsOpen(false); }} - defaultIconColor={value.in_use && value.in_use === "icon" ? value.icon?.color : undefined} - defaultOpen={ - value.in_use && value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON - } + value={value} + workspaceSlug={workspaceSlug?.toString() ?? ""} /> )} /> diff --git a/web/core/components/project/form.tsx b/web/core/components/project/form.tsx index 3a076ce29ef..91edd729dd5 100644 --- a/web/core/components/project/form.tsx +++ b/web/core/components/project/form.tsx @@ -6,41 +6,29 @@ import { Info, Lock } from "lucide-react"; // plane types import { IProject, IWorkspace } from "@plane/types"; // plane ui -import { - Button, - CustomSelect, - Input, - TextArea, - TOAST_TYPE, - setToast, - CustomEmojiIconPicker, - EmojiIconPickerTypes, - Tooltip, - // CustomSearchSelect, -} from "@plane/ui"; +import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast, Tooltip } from "@plane/ui"; // components -import { Logo } from "@/components/common"; import { ImagePickerPopover } from "@/components/core"; +import { ProjectLogoPicker } from "@/components/project"; // constants import { PROJECT_UPDATED } from "@/constants/event-tracker"; import { NETWORK_CHOICES } from "@/constants/project"; // helpers // import { TTimezone, TIME_ZONES } from "@/constants/timezones"; import { renderFormattedDate } from "@/helpers/date-time.helper"; -import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; import { getFileURL } from "@/helpers/file.helper"; // hooks import { useEventTracker, useProject } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; // services import { ProjectService } from "@/services/project"; +const projectService = new ProjectService(); export interface IProjectDetailsForm { project: IProject; workspaceSlug: string; projectId: string; isAdmin: boolean; } -const projectService = new ProjectService(); export const ProjectDetailsForm: FC = (props) => { const { project, workspaceSlug, projectId, isAdmin } = props; // states @@ -166,45 +154,31 @@ export const ProjectDetailsForm: FC = (props) => {
- Project cover image + {coverImage && ( + Project cover image + )}
( - setIsOpen(val)} - className="flex items-center justify-center" - buttonClassName="flex h-[52px] w-[52px] flex-shrink-0 items-center justify-center rounded-lg bg-white/10" - label={} + handleToggle={(val) => setIsOpen(val)} + buttonClassName="h-[52px] w-[52px] rounded-lg bg-white/10" + logoSize={28} onChange={(val) => { - let logoValue = {}; - - if (val?.type === "emoji") - logoValue = { - value: convertHexEmojiToDecimal(val.value.unified), - url: val.value.imageUrl, - }; - else if (val?.type === "icon") logoValue = val.value; - - onChange({ - in_use: val?.type, - [val?.type]: logoValue, - }); + onChange(val); setIsOpen(false); }} - defaultIconColor={value?.in_use && value.in_use === "icon" ? value?.icon?.color : undefined} - defaultOpen={ - value.in_use && value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON - } disabled={!isAdmin} + value={value} + workspaceSlug={workspaceSlug} /> )} /> diff --git a/web/core/components/project/index.ts b/web/core/components/project/index.ts index 5f47350a8f7..c942b897470 100644 --- a/web/core/components/project/index.ts +++ b/web/core/components/project/index.ts @@ -11,6 +11,7 @@ export * from "./form-loader"; export * from "./form"; export * from "./join-project-modal"; export * from "./leave-project-modal"; +export * from "./logo-picker"; export * from "./member-select"; export * from "./integration-card"; export * from "./member-list"; diff --git a/web/core/components/project/logo-picker.tsx b/web/core/components/project/logo-picker.tsx new file mode 100644 index 00000000000..f803e8cb062 --- /dev/null +++ b/web/core/components/project/logo-picker.tsx @@ -0,0 +1,92 @@ +"use client"; + +import React from "react"; +// plane helpers +import { convertHexEmojiToDecimal } from "@plane/helpers"; +// plane types +import { TLogoProps } from "@plane/types"; +import { EFileAssetType } from "@plane/types/src/enums"; +// plane ui +import { EmojiIconPickerTypes, ProjectLogoPickerDropdown } from "@plane/ui"; +// components +import { Logo } from "@/components/common"; +// helpers +import { cn } from "@/helpers/common.helper"; +// services +import { FileService } from "@/services/file.service"; +const fileService = new FileService(); + +type Props = { + buttonClassName?: string; + disabled?: boolean; + handleToggle: (val: boolean) => void; + isOpen: boolean; + logoSize?: number; + onChange: (val: TLogoProps) => void; + value: TLogoProps; + workspaceSlug: string; +}; + +export const ProjectLogoPicker: React.FC = (props) => { + const { + buttonClassName, + disabled = false, + handleToggle, + isOpen, + logoSize = 20, + onChange, + value, + workspaceSlug, + } = props; + + return ( + } + onChange={(val) => { + let logoValue: Omit = {}; + + if (val?.type === EmojiIconPickerTypes.EMOJI) { + logoValue = { + emoji: { + value: convertHexEmojiToDecimal(val.value.unified), + url: val.value.imageUrl, + }, + }; + } else if (val?.type === EmojiIconPickerTypes.ICON) { + logoValue = { + icon: val.value, + }; + } else if (val?.type === EmojiIconPickerTypes.IMAGE) { + logoValue = { + image: { + url: val.value, + }, + }; + } + + onChange({ + in_use: val?.type, + ...logoValue, + }); + handleToggle(false); + }} + defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined} + defaultOpen={value.in_use === "icon" ? EmojiIconPickerTypes.ICON : EmojiIconPickerTypes.EMOJI} + uploadFile={async (file) => { + const { asset_url } = await fileService.uploadWorkspaceAsset( + workspaceSlug, + { + entity_identifier: "", + entity_type: EFileAssetType.PROJECT_LOGO, + }, + file + ); + return asset_url; + }} + /> + ); +}; diff --git a/web/core/components/project/project-feature-update.tsx b/web/core/components/project/project-feature-update.tsx index ff10271d82c..10fd0210cd2 100644 --- a/web/core/components/project/project-feature-update.tsx +++ b/web/core/components/project/project-feature-update.tsx @@ -33,7 +33,10 @@ export const ProjectFeatureUpdate: FC = observer((props) => {
- Congrats! Project {" "} + Congrats! Project{" "} +
+ +
{" "}

{currentProjectDetails.name}

created.
diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-title.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-title.tsx index dfa939ce352..ead12f4fcf9 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-title.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-title.tsx @@ -33,7 +33,7 @@ export const FavoriteItemTitle: FC = observer((props) => { return ( - {icon} + {icon} {!isSidebarCollapsed && {title}} ); diff --git a/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx b/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx index e2f303afe78..ba74c2d5b60 100644 --- a/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx @@ -23,7 +23,7 @@ export const getFavoriteItemIcon = (type: string, logo?: TLogoProps | undefined)
{FAVORITE_ITEM_ICON[type] || }
-
+
{logo?.in_use ? ( ) : ( diff --git a/yarn.lock b/yarn.lock index 9dbe2e5ca10..f674ac53843 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2941,10 +2941,10 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958" integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg== -"@storybook/addon-actions@8.3.5": - version "8.3.5" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-8.3.5.tgz#03fdb891114439ed47cb7df6ef21826530449db7" - integrity sha512-t8D5oo+4XfD+F8091wLa2y/CDd/W2lExCeol5Vm1tp5saO+u6f2/d7iykLhTowWV84Uohi3D073uFeyTAlGebg== +"@storybook/addon-actions@8.3.6": + version "8.3.6" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-8.3.6.tgz#80c5dbfc2278d72dc461a954bb729165ee1dfecb" + integrity sha512-nOqgl0WoZK2KwjaABaXMoIgrIHOQl9inOzJvqQau0HOtsvnXGXYfJXYnpjZenoZDoZXKbUDl0U2haDFx2a2fJw== dependencies: "@storybook/global" "^5.0.0" "@types/uuid" "^9.0.1" @@ -3780,6 +3780,57 @@ dependencies: "@types/node" "*" +"@types/d3-array@^3.0.3": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-ease@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-interpolate@^3.0.1": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a" + integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== + +"@types/d3-scale@^4.0.2": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" + integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-shape@^3.1.0": + version "3.1.6" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72" + integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time@*", "@types/d3-time@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" + integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== + +"@types/d3-timer@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + "@types/debug@^4.0.0": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" @@ -5697,11 +5748,23 @@ d3-array@2, d3-array@^2.3.0: dependencies: internmap "^1.0.0" +"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + "d3-color@1 - 2", d3-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== +"d3-color@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + d3-delaunay@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-5.3.0.tgz#b47f05c38f854a4e7b3cea80e0bb12e57398772d" @@ -5709,11 +5772,21 @@ d3-delaunay@^5.3.0: dependencies: delaunator "4" +d3-ease@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + "d3-format@1 - 2": version "2.0.0" resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== +"d3-format@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + d3-format@^1.4.4: version "1.4.5" resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" @@ -5726,11 +5799,23 @@ d3-format@^1.4.4: dependencies: d3-color "1 - 2" +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + d3-path@1: version "1.0.9" resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + d3-scale-chromatic@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz#c13f3af86685ff91323dc2f0ebd2dabbd72d8bab" @@ -5750,6 +5835,17 @@ d3-scale@^3.2.3: d3-time "^2.1.1" d3-time-format "2 - 3" +d3-scale@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + d3-shape@^1.3.5: version "1.3.7" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" @@ -5757,6 +5853,13 @@ d3-shape@^1.3.5: dependencies: d3-path "1" +d3-shape@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + "d3-time-format@2 - 3", d3-time-format@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" @@ -5764,6 +5867,13 @@ d3-shape@^1.3.5: dependencies: d3-time "1 - 2" +"d3-time-format@2 - 4": + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + "d3-time@1 - 2", d3-time@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" @@ -5771,11 +5881,23 @@ d3-shape@^1.3.5: dependencies: d3-array "2" +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + d3-time@^1.0.10, d3-time@^1.0.11: version "1.1.0" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== +d3-timer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" @@ -5854,6 +5976,11 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +decimal.js-light@^2.4.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decimal.js@^10.4.3: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" @@ -6726,6 +6853,11 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +eventemitter3@^4.0.1: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -6810,6 +6942,11 @@ fast-deep-equal@^3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-equals@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.0.1.tgz#a4eefe3c5d1c0d021aeed0bc10ba5e0c12ee405d" + integrity sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ== + fast-fifo@^1.2.0, fast-fifo@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" @@ -7651,6 +7788,11 @@ internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + internmap@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" @@ -10180,7 +10322,7 @@ react-docgen@^7.0.0: loose-envify "^1.1.0" scheduler "^0.23.2" -react-dropzone@^14.2.3: +react-dropzone@^14.2.10, react-dropzone@^14.2.3: version "14.2.10" resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.10.tgz#b8d86775c616534b44a30bd9c962044d2925c1a2" integrity sha512-Y98LOCYxGO2jOFWREeKJlL7gbrHcOlTBp+9DCM1dh9XQ8+P/8ThhZT7kFb05C+bPcTXq/rixpU+5+LzwYrFLUw== @@ -10328,6 +10470,15 @@ react-selecto@^1.25.0: dependencies: selecto "~1.26.3" +react-smooth@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.1.tgz#6200d8699bfe051ae40ba187988323b1449eab1a" + integrity sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w== + dependencies: + fast-equals "^5.0.1" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + react-style-singleton@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" @@ -10411,6 +10562,27 @@ recast@^0.23.5: tiny-invariant "^1.3.3" tslib "^2.0.1" +recharts-scale@^0.4.4: + version "0.4.5" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" + integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== + dependencies: + decimal.js-light "^2.4.1" + +recharts@^2.12.7: + version "2.13.0" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.13.0.tgz#a293322ea357491393cc7ad6fcbb1e5f8e99bc93" + integrity sha512-sbfxjWQ+oLWSZEWmvbq/DFVdeRLqqA6d0CDjKx2PkxVVdoXo16jvENCE+u/x7HxOO+/fwx//nYRwb8p8X6s/lQ== + dependencies: + clsx "^2.0.0" + eventemitter3 "^4.0.1" + lodash "^4.17.21" + react-is "^18.3.1" + react-smooth "^4.0.0" + recharts-scale "^0.4.4" + tiny-invariant "^1.3.1" + victory-vendor "^36.6.8" + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -12191,6 +12363,26 @@ vfile@^5.0.0: unist-util-stringify-position "^3.0.0" vfile-message "^3.0.0" +victory-vendor@^36.6.8: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801" + integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ== + dependencies: + "@types/d3-array" "^3.0.3" + "@types/d3-ease" "^3.0.0" + "@types/d3-interpolate" "^3.0.1" + "@types/d3-scale" "^4.0.2" + "@types/d3-shape" "^3.1.0" + "@types/d3-time" "^3.0.0" + "@types/d3-timer" "^3.0.0" + d3-array "^3.1.6" + d3-ease "^3.0.1" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.1.0" + d3-time "^3.0.0" + d3-timer "^3.0.1" + vite-compatible-readable-stream@^3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz#27267aebbdc9893c0ddf65a421279cbb1e31d8cd"