diff --git a/apiserver/plane/app/serializers/cycle.py b/apiserver/plane/app/serializers/cycle.py index 1a9ce52d18c..97fd479600f 100644 --- a/apiserver/plane/app/serializers/cycle.py +++ b/apiserver/plane/app/serializers/cycle.py @@ -66,6 +66,7 @@ class Meta: "external_source", "external_id", "progress_snapshot", + "logo_props", # meta fields "is_favorite", "total_issues", diff --git a/apiserver/plane/app/serializers/module.py b/apiserver/plane/app/serializers/module.py index 6a0c4c94f3f..28d28d7dbc7 100644 --- a/apiserver/plane/app/serializers/module.py +++ b/apiserver/plane/app/serializers/module.py @@ -199,6 +199,7 @@ class Meta: "sort_order", "external_source", "external_id", + "logo_props", # computed fields "is_favorite", "total_issues", diff --git a/apiserver/plane/app/serializers/page.py b/apiserver/plane/app/serializers/page.py index 41f46c6e457..f13923831fc 100644 --- a/apiserver/plane/app/serializers/page.py +++ b/apiserver/plane/app/serializers/page.py @@ -39,6 +39,7 @@ class Meta: "created_by", "updated_by", "view_props", + "logo_props", ] read_only_fields = [ "workspace", diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index e0b28ac7bbc..5982daf7f97 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -231,6 +231,7 @@ def list(self, request, slug, project_id): "external_source", "external_id", "progress_snapshot", + "logo_props", # meta fields "is_favorite", "total_issues", @@ -356,6 +357,7 @@ def list(self, request, slug, project_id): "external_source", "external_id", "progress_snapshot", + "logo_props", # meta fields "is_favorite", "total_issues", @@ -403,6 +405,7 @@ def create(self, request, slug, project_id): "external_source", "external_id", "progress_snapshot", + "logo_props", # meta fields "is_favorite", "cancelled_issues", @@ -496,6 +499,7 @@ def partial_update(self, request, slug, project_id, pk): "external_source", "external_id", "progress_snapshot", + "logo_props", # meta fields "is_favorite", "total_issues", @@ -556,6 +560,7 @@ def retrieve(self, request, slug, project_id, pk): "external_id", "progress_snapshot", "sub_issues", + "logo_props", # meta fields "is_favorite", "total_issues", diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index f98e0fbc2d2..56267554d71 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -225,6 +225,7 @@ def create(self, request, slug, project_id): "sort_order", "external_source", "external_id", + "logo_props", # computed fields "is_favorite", "cancelled_issues", @@ -281,6 +282,7 @@ def list(self, request, slug, project_id): "sort_order", "external_source", "external_id", + "logo_props", # computed fields "total_issues", "is_favorite", @@ -465,6 +467,7 @@ def partial_update(self, request, slug, project_id, pk): "sort_order", "external_source", "external_id", + "logo_props", # computed fields "is_favorite", "cancelled_issues", diff --git a/packages/types/src/common.d.ts b/packages/types/src/common.d.ts index d347ecef1a4..6a8c725a82d 100644 --- a/packages/types/src/common.d.ts +++ b/packages/types/src/common.d.ts @@ -9,3 +9,15 @@ export type TPaginationInfo = { per_page?: number; total_results: number; }; + +export type TLogoProps = { + in_use: "emoji" | "icon"; + emoji?: { + value?: string; + url?: string; + }; + icon?: { + name?: string; + color?: string; + }; +}; diff --git a/packages/types/src/pages.d.ts b/packages/types/src/pages.d.ts index 4871ddc06e7..1c94dfc063c 100644 --- a/packages/types/src/pages.d.ts +++ b/packages/types/src/pages.d.ts @@ -1,3 +1,4 @@ +import { TLogoProps } from "./common"; import { EPageAccess } from "./enums"; export type TPage = { @@ -17,6 +18,7 @@ export type TPage = { updated_at: Date | undefined; updated_by: string | undefined; workspace: string | undefined; + logo_props: TLogoProps | undefined; }; // page filters diff --git a/packages/types/src/project/projects.d.ts b/packages/types/src/project/projects.d.ts index 459d9f0e2fe..ee974fd6392 100644 --- a/packages/types/src/project/projects.d.ts +++ b/packages/types/src/project/projects.d.ts @@ -6,21 +6,10 @@ import type { IUserMemberLite, IWorkspace, IWorkspaceLite, + TLogoProps, TStateGroups, } from ".."; -export type TProjectLogoProps = { - in_use: "emoji" | "icon"; - emoji?: { - value?: string; - url?: string; - }; - icon?: { - name?: string; - color?: string; - }; -}; - export interface IProject { archive_in: number; archived_at: string | null; @@ -46,7 +35,7 @@ export interface IProject { is_deployed: boolean; is_favorite: boolean; is_member: boolean; - logo_props: TProjectLogoProps; + logo_props: TLogoProps; member_role: EUserProjectRoles | null; members: IProjectMemberLite[]; name: string; diff --git a/packages/types/src/views.d.ts b/packages/types/src/views.d.ts index f9f7ee3852f..9415f7488f2 100644 --- a/packages/types/src/views.d.ts +++ b/packages/types/src/views.d.ts @@ -1,3 +1,4 @@ +import { TLogoProps } from "./common"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, @@ -21,4 +22,5 @@ export interface IProjectView { query_data: IIssueFilterOptions; project: string; workspace: string; + logo_props: TLogoProps | undefined; } diff --git a/packages/ui/package.json b/packages/ui/package.json index 62c335839b3..3e741edd4c9 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -26,6 +26,7 @@ "@popperjs/core": "^2.11.8", "clsx": "^2.0.0", "emoji-picker-react": "^4.5.16", + "lucide-react": "^0.379.0", "react-color": "^2.19.3", "react-dom": "^18.2.0", "react-popper": "^2.3.0", diff --git a/packages/ui/src/control-link/control-link.tsx b/packages/ui/src/control-link/control-link.tsx index 61426e44bfb..df195847656 100644 --- a/packages/ui/src/control-link/control-link.tsx +++ b/packages/ui/src/control-link/control-link.tsx @@ -2,7 +2,7 @@ import * as React from "react"; export type TControlLink = React.AnchorHTMLAttributes & { href: string; - onClick: () => void; + onClick: (event: React.MouseEvent) => void; children: React.ReactNode; target?: string; disabled?: boolean; @@ -17,7 +17,7 @@ export const ControlLink = React.forwardRef((pr const clickCondition = (event.metaKey || event.ctrlKey) && event.button === LEFT_CLICK_EVENT_CODE; if (!clickCondition) { event.preventDefault(); - onClick(); + onClick(event); } }; diff --git a/packages/ui/src/emoji/emoji-icon-helper.tsx b/packages/ui/src/emoji/emoji-icon-helper.tsx new file mode 100644 index 00000000000..533f025d1e5 --- /dev/null +++ b/packages/ui/src/emoji/emoji-icon-helper.tsx @@ -0,0 +1,100 @@ +import { Placement } from "@popperjs/core"; +import { EmojiClickData, Theme } from "emoji-picker-react"; + +export enum EmojiIconPickerTypes { + EMOJI = "emoji", + ICON = "icon", +} + +export const TABS_LIST = [ + { + key: EmojiIconPickerTypes.EMOJI, + title: "Emojis", + }, + { + key: EmojiIconPickerTypes.ICON, + title: "Icons", + }, +]; + +export type TChangeHandlerProps = + | { + type: EmojiIconPickerTypes.EMOJI; + value: EmojiClickData; + } + | { + type: EmojiIconPickerTypes.ICON; + value: { + name: string; + color: string; + }; + }; + +export type TCustomEmojiPicker = { + isOpen: boolean; + handleToggle: (value: boolean) => void; + buttonClassName?: string; + className?: string; + closeOnSelect?: boolean; + defaultIconColor?: string; + defaultOpen?: EmojiIconPickerTypes; + disabled?: boolean; + dropdownClassName?: string; + label: React.ReactNode; + onChange: (value: TChangeHandlerProps) => void; + placement?: Placement; + searchPlaceholder?: string; + theme?: Theme; + iconType?: "material" | "lucide"; +}; + +export const DEFAULT_COLORS = ["#95999f", "#6d7b8a", "#5e6ad2", "#02b5ed", "#02b55c", "#f2be02", "#e57a00", "#f38e82"]; + +export type TIconsListProps = { + defaultColor: string; + onChange: (val: { name: string; color: string }) => void; +}; + +/** + * Adjusts the given hex color to ensure it has enough contrast. + * @param {string} hex - The hex color code input by the user. + * @returns {string} - The adjusted hex color code. + */ +export const adjustColorForContrast = (hex: string): string => { + // Ensure hex color is valid + if (!/^#([0-9A-F]{3}){1,2}$/i.test(hex)) { + throw new Error("Invalid hex color code"); + } + + // Convert hex to RGB + let r = 0, + g = 0, + b = 0; + if (hex.length === 4) { + r = parseInt(hex[1] + hex[1], 16); + g = parseInt(hex[2] + hex[2], 16); + b = parseInt(hex[3] + hex[3], 16); + } else if (hex.length === 7) { + r = parseInt(hex[1] + hex[2], 16); + g = parseInt(hex[3] + hex[4], 16); + b = parseInt(hex[5] + hex[6], 16); + } + + // Calculate luminance + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + + // If the color is too light, darken it + if (luminance > 0.5) { + r = Math.max(0, r - 50); + g = Math.max(0, g - 50); + b = Math.max(0, b - 50); + } + + // Convert RGB back to hex + const toHex = (value: number): string => { + const hex = value.toString(16); + return hex.length === 1 ? "0" + hex : hex; + }; + + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; +}; diff --git a/packages/ui/src/emoji/emoji-icon-picker-new.tsx b/packages/ui/src/emoji/emoji-icon-picker-new.tsx new file mode 100644 index 00000000000..557b39658c5 --- /dev/null +++ b/packages/ui/src/emoji/emoji-icon-picker-new.tsx @@ -0,0 +1,135 @@ +import React, { useRef, useState } from "react"; +import { usePopper } from "react-popper"; +import { Popover, Tab } from "@headlessui/react"; +import EmojiPicker from "emoji-picker-react"; +// helpers +import { cn } from "../../helpers"; +// hooks +import useOutsideClickDetector from "../hooks/use-outside-click-detector"; +import { LucideIconsList } from "./lucide-icons-list"; +// helpers +import { EmojiIconPickerTypes, TABS_LIST, TCustomEmojiPicker } from "./emoji-icon-helper"; + +export const EmojiIconPicker: React.FC = (props) => { + const { + isOpen, + handleToggle, + buttonClassName, + className, + closeOnSelect = true, + defaultIconColor = "#6d7b8a", + defaultOpen = EmojiIconPickerTypes.EMOJI, + disabled = false, + dropdownClassName, + label, + onChange, + placement = "bottom-start", + searchPlaceholder = "Search", + theme, + } = props; + // refs + const containerRef = useRef(null); + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement, + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 20, + }, + }, + ], + }); + + // close dropdown on outside click + useOutsideClickDetector(containerRef, () => handleToggle(false)); + + return ( + + <> + + + + {isOpen && ( + +
+ tab.key === defaultOpen)} + > + + {TABS_LIST.map((tab) => ( + + cn("py-1 text-sm rounded border border-custom-border-200", { + "bg-custom-background-80": selected, + "hover:bg-custom-background-90 focus:bg-custom-background-90": !selected, + }) + } + > + {tab.title} + + ))} + + + + { + onChange({ + type: EmojiIconPickerTypes.EMOJI, + value: val, + }); + if (closeOnSelect) close(); + }} + height="20rem" + width="100%" + theme={theme} + searchPlaceholder={searchPlaceholder} + previewConfig={{ + showPreview: false, + }} + /> + + + { + onChange({ + type: EmojiIconPickerTypes.ICON, + value: val, + }); + if (closeOnSelect) close(); + }} + /> + + + +
+
+ )} + +
+ ); +}; diff --git a/packages/ui/src/emoji/emoji-icon-picker.tsx b/packages/ui/src/emoji/emoji-icon-picker.tsx index 5bfcdbe1722..c531dd16879 100644 --- a/packages/ui/src/emoji/emoji-icon-picker.tsx +++ b/packages/ui/src/emoji/emoji-icon-picker.tsx @@ -1,63 +1,23 @@ -import React, { useState } from "react"; +import React, { useRef, useState } from "react"; import { usePopper } from "react-popper"; -import EmojiPicker, { EmojiClickData, Theme } from "emoji-picker-react"; +import EmojiPicker from "emoji-picker-react"; import { Popover, Tab } from "@headlessui/react"; -import { Placement } from "@popperjs/core"; // components import { IconsList } from "./icons-list"; // helpers import { cn } from "../../helpers"; - -export enum EmojiIconPickerTypes { - EMOJI = "emoji", - ICON = "icon", -} - -type TChangeHandlerProps = - | { - type: EmojiIconPickerTypes.EMOJI; - value: EmojiClickData; - } - | { - type: EmojiIconPickerTypes.ICON; - value: { - name: string; - color: string; - }; - }; - -export type TCustomEmojiPicker = { - buttonClassName?: string; - className?: string; - closeOnSelect?: boolean; - defaultIconColor?: string; - defaultOpen?: EmojiIconPickerTypes; - disabled?: boolean; - dropdownClassName?: string; - label: React.ReactNode; - onChange: (value: TChangeHandlerProps) => void; - placement?: Placement; - searchPlaceholder?: string; - theme?: Theme; -}; - -const TABS_LIST = [ - { - key: EmojiIconPickerTypes.EMOJI, - title: "Emojis", - }, - { - key: EmojiIconPickerTypes.ICON, - title: "Icons", - }, -]; +// hooks +import useOutsideClickDetector from "../hooks/use-outside-click-detector"; +import { EmojiIconPickerTypes, TABS_LIST, TCustomEmojiPicker } from "./emoji-icon-helper"; export const CustomEmojiIconPicker: React.FC = (props) => { const { + isOpen, + handleToggle, buttonClassName, className, closeOnSelect = true, - defaultIconColor = "#5f5f5f", + defaultIconColor = "#6d7b8a", defaultOpen = EmojiIconPickerTypes.EMOJI, disabled = false, dropdownClassName, @@ -68,6 +28,7 @@ export const CustomEmojiIconPicker: React.FC = (props) => { theme, } = props; // refs + const containerRef = useRef(null); const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); // popper-js @@ -83,21 +44,25 @@ export const CustomEmojiIconPicker: React.FC = (props) => { ], }); + // close dropdown on outside click + useOutsideClickDetector(containerRef, () => handleToggle(false)); + return ( - {({ close }) => ( - <> - - - - + <> + + + + {isOpen && ( +
= (props) => { )} > tab.key === defaultOpen)} @@ -162,8 +128,8 @@ export const CustomEmojiIconPicker: React.FC = (props) => {
- - )} + )} +
); }; diff --git a/packages/ui/src/emoji/icons-list.tsx b/packages/ui/src/emoji/icons-list.tsx index f55da881b47..0352e1ec8ef 100644 --- a/packages/ui/src/emoji/icons-list.tsx +++ b/packages/ui/src/emoji/icons-list.tsx @@ -3,15 +3,11 @@ import React, { useEffect, useState } from "react"; import { Input } from "../form-fields"; // helpers import { cn } from "../../helpers"; -// constants +import { DEFAULT_COLORS, TIconsListProps, adjustColorForContrast } from "./emoji-icon-helper"; +// icons import { MATERIAL_ICONS_LIST } from "./icons"; - -type TIconsListProps = { - defaultColor: string; - onChange: (val: { name: string; color: string }) => void; -}; - -const DEFAULT_COLORS = ["#ff6b00", "#8cc1ff", "#fcbe1d", "#18904f", "#adf672", "#05c3ff", "#5f5f5f"]; +import { InfoIcon } from "../icons"; +import { Search } from "lucide-react"; export const IconsList: React.FC = (props) => { const { defaultColor, onChange } = props; @@ -19,6 +15,8 @@ export const IconsList: React.FC = (props) => { const [activeColor, setActiveColor] = useState(defaultColor); const [showHexInput, setShowHexInput] = useState(false); const [hexValue, setHexValue] = useState(""); + const [isInputFocused, setIsInputFocused] = useState(false); + const [query, setQuery] = useState(""); useEffect(() => { if (DEFAULT_COLORS.includes(defaultColor.toLowerCase())) setShowHexInput(false); @@ -28,11 +26,28 @@ export const IconsList: React.FC = (props) => { } }, [defaultColor]); + const filteredArray = MATERIAL_ICONS_LIST.filter((icon) => icon.name.toLowerCase().includes(query.toLowerCase())); + return ( <> -
+
+
setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + > + + setQuery(e.target.value)} + className="text-[1rem] border-none p-0 h-full w-full " + /> +
+
+
{showHexInput ? ( -
+
= (props) => { onChange={(e) => { const value = e.target.value; setHexValue(value); - if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(`#${value}`); + if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(adjustColorForContrast(`#${value}`)); }} className="flex-grow pl-0 text-xs text-custom-text-200" mode="true-transparent" @@ -59,7 +74,7 @@ export const IconsList: React.FC = (props) => {
-
- {MATERIAL_ICONS_LIST.map((icon) => ( +
+ +

Colors will be adjusted to ensure sufficient contrast.

+
+
+ {filteredArray.map((icon) => ( diff --git a/packages/ui/src/emoji/icons.ts b/packages/ui/src/emoji/icons.ts index 72aacf18bb7..3d650e244ef 100644 --- a/packages/ui/src/emoji/icons.ts +++ b/packages/ui/src/emoji/icons.ts @@ -1,3 +1,156 @@ +import { + Activity, + Airplay, + AlertCircle, + AlertOctagon, + AlertTriangle, + AlignCenter, + AlignJustify, + AlignLeft, + AlignRight, + Anchor, + Aperture, + Archive, + ArrowDown, + ArrowLeft, + ArrowRight, + ArrowUp, + AtSign, + Award, + BarChart, + BarChart2, + Battery, + BatteryCharging, + Bell, + BellOff, + Book, + Bookmark, + BookOpen, + Box, + Briefcase, + Calendar, + Camera, + CameraOff, + Cast, + Check, + CheckCircle, + CheckSquare, + ChevronDown, + ChevronLeft, + ChevronRight, + ChevronUp, + Clipboard, + Clock, + Cloud, + CloudDrizzle, + CloudLightning, + CloudOff, + CloudRain, + CloudSnow, + Code, + Codepen, + Codesandbox, + Coffee, + Columns, + Command, + Compass, + Copy, + CornerDownLeft, + CornerDownRight, + CornerLeftDown, + CornerLeftUp, + CornerRightDown, + CornerRightUp, + CornerUpLeft, + CornerUpRight, + Cpu, + CreditCard, + Crop, + Crosshair, + Database, + Delete, + Disc, + Divide, + DivideCircle, + DivideSquare, + DollarSign, + Download, + DownloadCloud, + Dribbble, + Droplet, + Edit, + Edit2, + Edit3, + ExternalLink, + Eye, + EyeOff, + Facebook, + FastForward, + Feather, + Figma, + File, + FileMinus, + FilePlus, + FileText, + Film, + Filter, + Flag, + Folder, + FolderMinus, + FolderPlus, + Framer, + Frown, + Gift, + GitBranch, + GitCommit, + GitMerge, + GitPullRequest, + Github, + Gitlab, + Globe, + Grid, + HardDrive, + Hash, + Headphones, + Heart, + HelpCircle, + Hexagon, + Home, + Image, + Inbox, + Info, + Instagram, + Italic, + Key, + Layers, + Layout, + LifeBuoy, + Link, + Link2, + Linkedin, + List, + Loader, + Lock, + LogIn, + LogOut, + Mail, + Map, + MapPin, + Maximize, + Maximize2, + Meh, + Menu, + MessageCircle, + MessageSquare, + Mic, + MicOff, + Minimize, + Minimize2, + Minus, + MinusCircle, + MinusSquare, +} from "lucide-react"; + export const MATERIAL_ICONS_LIST = [ { name: "search", @@ -603,3 +756,156 @@ export const MATERIAL_ICONS_LIST = [ name: "skull", }, ]; + +export const LUCIDE_ICONS_LIST = [ + { name: "Activity", element: Activity }, + { name: "Airplay", element: Airplay }, + { name: "AlertCircle", element: AlertCircle }, + { name: "AlertOctagon", element: AlertOctagon }, + { name: "AlertTriangle", element: AlertTriangle }, + { name: "AlignCenter", element: AlignCenter }, + { name: "AlignJustify", element: AlignJustify }, + { name: "AlignLeft", element: AlignLeft }, + { name: "AlignRight", element: AlignRight }, + { name: "Anchor", element: Anchor }, + { name: "Aperture", element: Aperture }, + { name: "Archive", element: Archive }, + { name: "ArrowDown", element: ArrowDown }, + { name: "ArrowLeft", element: ArrowLeft }, + { name: "ArrowRight", element: ArrowRight }, + { name: "ArrowUp", element: ArrowUp }, + { name: "AtSign", element: AtSign }, + { name: "Award", element: Award }, + { name: "BarChart", element: BarChart }, + { name: "BarChart2", element: BarChart2 }, + { name: "Battery", element: Battery }, + { name: "BatteryCharging", element: BatteryCharging }, + { name: "Bell", element: Bell }, + { name: "BellOff", element: BellOff }, + { name: "Book", element: Book }, + { name: "Bookmark", element: Bookmark }, + { name: "BookOpen", element: BookOpen }, + { name: "Box", element: Box }, + { name: "Briefcase", element: Briefcase }, + { name: "Calendar", element: Calendar }, + { name: "Camera", element: Camera }, + { name: "CameraOff", element: CameraOff }, + { name: "Cast", element: Cast }, + { name: "Check", element: Check }, + { name: "CheckCircle", element: CheckCircle }, + { name: "CheckSquare", element: CheckSquare }, + { name: "ChevronDown", element: ChevronDown }, + { name: "ChevronLeft", element: ChevronLeft }, + { name: "ChevronRight", element: ChevronRight }, + { name: "ChevronUp", element: ChevronUp }, + { name: "Clipboard", element: Clipboard }, + { name: "Clock", element: Clock }, + { name: "Cloud", element: Cloud }, + { name: "CloudDrizzle", element: CloudDrizzle }, + { name: "CloudLightning", element: CloudLightning }, + { name: "CloudOff", element: CloudOff }, + { name: "CloudRain", element: CloudRain }, + { name: "CloudSnow", element: CloudSnow }, + { name: "Code", element: Code }, + { name: "Codepen", element: Codepen }, + { name: "Codesandbox", element: Codesandbox }, + { name: "Coffee", element: Coffee }, + { name: "Columns", element: Columns }, + { name: "Command", element: Command }, + { name: "Compass", element: Compass }, + { name: "Copy", element: Copy }, + { name: "CornerDownLeft", element: CornerDownLeft }, + { name: "CornerDownRight", element: CornerDownRight }, + { name: "CornerLeftDown", element: CornerLeftDown }, + { name: "CornerLeftUp", element: CornerLeftUp }, + { name: "CornerRightDown", element: CornerRightDown }, + { name: "CornerRightUp", element: CornerRightUp }, + { name: "CornerUpLeft", element: CornerUpLeft }, + { name: "CornerUpRight", element: CornerUpRight }, + { name: "Cpu", element: Cpu }, + { name: "CreditCard", element: CreditCard }, + { name: "Crop", element: Crop }, + { name: "Crosshair", element: Crosshair }, + { name: "Database", element: Database }, + { name: "Delete", element: Delete }, + { name: "Disc", element: Disc }, + { name: "Divide", element: Divide }, + { name: "DivideCircle", element: DivideCircle }, + { name: "DivideSquare", element: DivideSquare }, + { name: "DollarSign", element: DollarSign }, + { name: "Download", element: Download }, + { name: "DownloadCloud", element: DownloadCloud }, + { name: "Dribbble", element: Dribbble }, + { name: "Droplet", element: Droplet }, + { name: "Edit", element: Edit }, + { name: "Edit2", element: Edit2 }, + { name: "Edit3", element: Edit3 }, + { name: "ExternalLink", element: ExternalLink }, + { name: "Eye", element: Eye }, + { name: "EyeOff", element: EyeOff }, + { name: "Facebook", element: Facebook }, + { name: "FastForward", element: FastForward }, + { name: "Feather", element: Feather }, + { name: "Figma", element: Figma }, + { name: "File", element: File }, + { name: "FileMinus", element: FileMinus }, + { name: "FilePlus", element: FilePlus }, + { name: "FileText", element: FileText }, + { name: "Film", element: Film }, + { name: "Filter", element: Filter }, + { name: "Flag", element: Flag }, + { name: "Folder", element: Folder }, + { name: "FolderMinus", element: FolderMinus }, + { name: "FolderPlus", element: FolderPlus }, + { name: "Framer", element: Framer }, + { name: "Frown", element: Frown }, + { name: "Gift", element: Gift }, + { name: "GitBranch", element: GitBranch }, + { name: "GitCommit", element: GitCommit }, + { name: "GitMerge", element: GitMerge }, + { name: "GitPullRequest", element: GitPullRequest }, + { name: "Github", element: Github }, + { name: "Gitlab", element: Gitlab }, + { name: "Globe", element: Globe }, + { name: "Grid", element: Grid }, + { name: "HardDrive", element: HardDrive }, + { name: "Hash", element: Hash }, + { name: "Headphones", element: Headphones }, + { name: "Heart", element: Heart }, + { name: "HelpCircle", element: HelpCircle }, + { name: "Hexagon", element: Hexagon }, + { name: "Home", element: Home }, + { name: "Image", element: Image }, + { name: "Inbox", element: Inbox }, + { name: "Info", element: Info }, + { name: "Instagram", element: Instagram }, + { name: "Italic", element: Italic }, + { name: "Key", element: Key }, + { name: "Layers", element: Layers }, + { name: "Layout", element: Layout }, + { name: "LifeBuoy", element: LifeBuoy }, + { name: "Link", element: Link }, + { name: "Link2", element: Link2 }, + { name: "Linkedin", element: Linkedin }, + { name: "List", element: List }, + { name: "Loader", element: Loader }, + { name: "Lock", element: Lock }, + { name: "LogIn", element: LogIn }, + { name: "LogOut", element: LogOut }, + { name: "Mail", element: Mail }, + { name: "Map", element: Map }, + { name: "MapPin", element: MapPin }, + { name: "Maximize", element: Maximize }, + { name: "Maximize2", element: Maximize2 }, + { name: "Meh", element: Meh }, + { name: "Menu", element: Menu }, + { name: "MessageCircle", element: MessageCircle }, + { name: "MessageSquare", element: MessageSquare }, + { name: "Mic", element: Mic }, + { name: "MicOff", element: MicOff }, + { name: "Minimize", element: Minimize }, + { name: "Minimize2", element: Minimize2 }, + { name: "Minus", element: Minus }, + { name: "MinusCircle", element: MinusCircle }, + { name: "MinusSquare", element: MinusSquare }, +]; diff --git a/packages/ui/src/emoji/index.ts b/packages/ui/src/emoji/index.ts index 97345413903..128b802925e 100644 --- a/packages/ui/src/emoji/index.ts +++ b/packages/ui/src/emoji/index.ts @@ -1 +1,4 @@ +export * from "./emoji-icon-picker-new"; export * from "./emoji-icon-picker"; +export * from "./emoji-icon-helper"; +export * from "./icons"; diff --git a/packages/ui/src/emoji/lucide-icons-list.tsx b/packages/ui/src/emoji/lucide-icons-list.tsx new file mode 100644 index 00000000000..799f0919dcc --- /dev/null +++ b/packages/ui/src/emoji/lucide-icons-list.tsx @@ -0,0 +1,128 @@ +import React, { useEffect, useState } from "react"; +// components +import { Input } from "../form-fields"; +// helpers +import { cn } from "../../helpers"; +import { DEFAULT_COLORS, TIconsListProps, adjustColorForContrast } from "./emoji-icon-helper"; +// icons +import { InfoIcon } from "../icons"; +// constants +import { LUCIDE_ICONS_LIST } from "./icons"; +import { Search } from "lucide-react"; + +export const LucideIconsList: React.FC = (props) => { + const { defaultColor, onChange } = props; + // states + const [activeColor, setActiveColor] = useState(defaultColor); + const [showHexInput, setShowHexInput] = useState(false); + const [hexValue, setHexValue] = useState(""); + const [isInputFocused, setIsInputFocused] = useState(false); + const [query, setQuery] = useState(""); + + useEffect(() => { + if (DEFAULT_COLORS.includes(defaultColor.toLowerCase())) setShowHexInput(false); + else { + setHexValue(defaultColor.slice(1, 7)); + setShowHexInput(true); + } + }, [defaultColor]); + + const filteredArray = LUCIDE_ICONS_LIST.filter((icon) => icon.name.toLowerCase().includes(query.toLowerCase())); + + return ( + <> +
+
setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + > + + setQuery(e.target.value)} + className="text-[1rem] border-none p-0 h-full w-full " + /> +
+
+
+ {showHexInput ? ( +
+ + HEX + # + { + const value = e.target.value; + setHexValue(value); + if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(adjustColorForContrast(`#${value}`)); + }} + className="flex-grow pl-0 text-xs text-custom-text-200" + mode="true-transparent" + autoFocus + /> +
+ ) : ( + DEFAULT_COLORS.map((curCol) => ( + + )) + )} + +
+
+ +

Colors will be adjusted to ensure sufficient contrast.

+
+
+ {filteredArray.map((icon) => ( + + ))} +
+ + ); +}; diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts index 5028848d8e2..c51375282ed 100644 --- a/packages/ui/src/icons/index.ts +++ b/packages/ui/src/icons/index.ts @@ -19,3 +19,4 @@ export * from "./priority-icon"; export * from "./related-icon"; export * from "./side-panel-icon"; export * from "./transfer-icon"; +export * from "./info-icon"; diff --git a/packages/ui/src/icons/info-icon.tsx b/packages/ui/src/icons/info-icon.tsx new file mode 100644 index 00000000000..5dbc7f75649 --- /dev/null +++ b/packages/ui/src/icons/info-icon.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const InfoIcon: React.FC = ({ className = "text-current", ...rest }) => ( + + + + + +); diff --git a/space/components/common/project-logo.tsx b/space/components/common/project-logo.tsx index 9b69e96167d..dfb3a4b80e2 100644 --- a/space/components/common/project-logo.tsx +++ b/space/components/common/project-logo.tsx @@ -1,11 +1,11 @@ +// types +import { TLogoProps } from "@plane/types"; // helpers -import { TProjectLogoProps } from "@plane/types"; import { cn } from "@/helpers/common.helper"; -// types type Props = { className?: string; - logo: TProjectLogoProps; + logo: TLogoProps; }; export const ProjectLogo: React.FC = (props) => { diff --git a/space/types/project.d.ts b/space/types/project.d.ts index 99dbfec8bb5..90c89ed80c4 100644 --- a/space/types/project.d.ts +++ b/space/types/project.d.ts @@ -1,4 +1,4 @@ -import { TProjectLogoProps } from "@plane/types"; +import { TLogoProps } from "@plane/types"; export type TWorkspaceDetails = { name: string; @@ -19,7 +19,7 @@ export type TProjectDetails = { identifier: string; name: string; cover_image: string | undefined; - logo_props: TProjectLogoProps; + logo_props: TLogoProps; description: string; }; diff --git a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx index 0a61e06acef..d7080746763 100644 --- a/web/components/analytics/custom-analytics/sidebar/projects-list.tsx +++ b/web/components/analytics/custom-analytics/sidebar/projects-list.tsx @@ -1,10 +1,11 @@ import { observer } from "mobx-react"; -// hooks // icons import { Contrast, LayoutGrid, Users } from "lucide-react"; +// components +import { Logo } from "@/components/common"; // helpers -import { ProjectLogo } from "@/components/project"; import { truncateText } from "@/helpers/string.helper"; +// hooks import { useProject } from "@/hooks/store"; type Props = { @@ -29,7 +30,7 @@ export const CustomAnalyticsSidebarProjectsList: React.FC = observer((pro
- +

{truncateText(project.name, 20)}

diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx index 6954a897368..ec1eb3ee35b 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar-header.tsx @@ -1,13 +1,13 @@ import { observer } from "mobx-react"; import { useRouter } from "next/router"; -// hooks -import { ProjectLogo } from "@/components/project"; +// components +import { Logo } from "@/components/common"; +// constants import { NETWORK_CHOICES } from "@/constants/project"; +// helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; +// hooks import { useCycle, useMember, useModule, useProject } from "@/hooks/store"; -// components -// helpers -// constants export const CustomAnalyticsSidebarHeader = observer(() => { const router = useRouter(); @@ -84,7 +84,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
{projectDetails && ( - + )}

{projectDetails?.name}

diff --git a/web/components/common/index.ts b/web/components/common/index.ts index 816562488be..1ca40f81060 100644 --- a/web/components/common/index.ts +++ b/web/components/common/index.ts @@ -3,3 +3,4 @@ export * from "./empty-state"; export * from "./latest-feature-block"; export * from "./breadcrumb-link"; export * from "./logo-spinner"; +export * from "./logo"; diff --git a/web/components/common/logo.tsx b/web/components/common/logo.tsx new file mode 100644 index 00000000000..d091dedd4c1 --- /dev/null +++ b/web/components/common/logo.tsx @@ -0,0 +1,69 @@ +import { FC } from "react"; +// emoji-picker-react +import { Emoji } from "emoji-picker-react"; +// import { icons } from "lucide-react"; +import { TLogoProps } from "@plane/types"; +// helpers +import { LUCIDE_ICONS_LIST } from "@plane/ui"; +import { emojiCodeToUnicode } from "@/helpers/emoji.helper"; + +type Props = { + logo: TLogoProps; + size?: number; + type?: "lucide" | "material"; +}; + +export const Logo: FC = (props) => { + const { logo, size = 16, type = "material" } = props; + + // destructuring the logo object + const { in_use, emoji, icon } = logo; + + // derived values + const value = in_use === "emoji" ? emoji?.value : icon?.name; + const color = icon?.color; + const lucideIcon = LUCIDE_ICONS_LIST.find((item) => item.name === value); + + // if no value, return empty fragment + if (!value) return <>; + + // emoji + if (in_use === "emoji") { + return ; + } + + // icon + if (in_use === "icon") { + return ( + <> + {type === "lucide" ? ( + <> + {lucideIcon && ( + + )} + + ) : ( + + {value} + + )} + + ); + } + + // if no value, return empty fragment + return <>; +}; diff --git a/web/components/core/list/list-item.tsx b/web/components/core/list/list-item.tsx index 89b23dbb505..8527d56b501 100644 --- a/web/components/core/list/list-item.tsx +++ b/web/components/core/list/list-item.tsx @@ -1,7 +1,7 @@ import React, { FC } from "react"; -import Link from "next/link"; +import { useRouter } from "next/router"; // ui -import { Tooltip } from "@plane/ui"; +import { ControlLink, Tooltip } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; @@ -14,6 +14,7 @@ interface IListItemProps { actionableItems?: JSX.Element; isMobile?: boolean; parentRef: React.RefObject; + disableLink?: boolean; className?: string; } @@ -27,12 +28,22 @@ export const ListItem: FC = (props) => { onItemClick, isMobile = false, parentRef, + disableLink = false, className = "", } = props; + // router + const router = useRouter(); + + // handlers + const handleControlLinkClick = (e: React.MouseEvent) => { + if (onItemClick) onItemClick(e); + else router.push(itemLink); + }; + return (
- +
= (props) => {
- + {actionableItems && (
diff --git a/web/components/cycles/list/cycles-list-item.tsx b/web/components/cycles/list/cycles-list-item.tsx index b2d9cb8828e..414c8081a19 100644 --- a/web/components/cycles/list/cycles-list-item.tsx +++ b/web/components/cycles/list/cycles-list-item.tsx @@ -77,13 +77,18 @@ export const CyclesListItem: FC = observer((props) => { } }; + // handlers + const handleArchivedCycleClick = (e: MouseEvent) => { + openCycleOverview(e); + }; + + const handleItemClick = cycleDetails.archived_at ? handleArchivedCycleClick : undefined; + return ( { - if (cycleDetails.archived_at) openCycleOverview(e); - }} + onItemClick={handleItemClick} className={className} prependTitleElement={ diff --git a/web/components/dashboard/widgets/recent-projects.tsx b/web/components/dashboard/widgets/recent-projects.tsx index 24c85b6f228..803edc8e28a 100644 --- a/web/components/dashboard/widgets/recent-projects.tsx +++ b/web/components/dashboard/widgets/recent-projects.tsx @@ -7,8 +7,8 @@ import { TRecentProjectsWidgetResponse } from "@plane/types"; // ui import { Avatar, AvatarGroup } from "@plane/ui"; // components +import { Logo } from "@/components/common"; import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets"; -import { ProjectLogo } from "@/components/project"; // constants import { PROJECT_BACKGROUND_COLORS } from "@/constants/dashboard"; import { EUserWorkspaceRoles } from "@/constants/workspace"; @@ -38,7 +38,7 @@ const ProjectListItem: React.FC = observer((props) => { className={`grid h-[3.375rem] w-[3.375rem] flex-shrink-0 place-items-center rounded border border-transparent ${randomBgColor}`} >
- +
diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx index c6a0c1bb4a0..ea7dea5490f 100644 --- a/web/components/dropdowns/project.tsx +++ b/web/components/dropdowns/project.tsx @@ -6,7 +6,7 @@ import { Combobox } from "@headlessui/react"; // types import { IProject } from "@plane/types"; // components -import { ProjectLogo } from "@/components/project"; +import { Logo } from "@/components/common"; // helpers import { cn } from "@/helpers/common.helper"; // hooks @@ -83,7 +83,7 @@ export const ProjectDropdown: React.FC = observer((props) => {
{projectDetails && ( - + )} {projectDetails?.name} @@ -157,7 +157,7 @@ export const ProjectDropdown: React.FC = observer((props) => { > {!hideIcon && selectedProject && ( - + )} {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index e0d7e3c5092..c26be96064c 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -10,9 +10,8 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui"; // components import { ProjectAnalyticsModal } from "@/components/analytics"; -import { BreadcrumbLink } from "@/components/common"; +import { BreadcrumbLink, Logo } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; -import { ProjectLogo } from "@/components/project"; // constants import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; import { EUserProjectRoles } from "@/constants/project"; @@ -170,7 +169,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { icon={ currentProjectDetails && ( - + ) } diff --git a/web/components/headers/cycles.tsx b/web/components/headers/cycles.tsx index 7b78e27fd23..76493bd5109 100644 --- a/web/components/headers/cycles.tsx +++ b/web/components/headers/cycles.tsx @@ -4,9 +4,8 @@ import { useRouter } from "next/router"; // ui import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui"; // components -import { BreadcrumbLink } from "@/components/common"; +import { BreadcrumbLink, Logo } from "@/components/common"; import { CyclesViewHeader } from "@/components/cycles"; -import { ProjectLogo } from "@/components/project"; // constants import { EUserProjectRoles } from "@/constants/project"; // hooks @@ -41,7 +40,7 @@ export const CyclesHeader: FC = observer(() => { icon={ currentProjectDetails && ( - + ) } diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 538eca2cde8..119cb9a9447 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -10,9 +10,8 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui"; // components import { ProjectAnalyticsModal } from "@/components/analytics"; -import { BreadcrumbLink } from "@/components/common"; +import { BreadcrumbLink, Logo } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; -import { ProjectLogo } from "@/components/project"; // constants import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; import { EUserProjectRoles } from "@/constants/project"; @@ -170,7 +169,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { icon={ currentProjectDetails && ( - + ) } diff --git a/web/components/headers/modules-list.tsx b/web/components/headers/modules-list.tsx index 90866d73ebb..0e1fd53fc0a 100644 --- a/web/components/headers/modules-list.tsx +++ b/web/components/headers/modules-list.tsx @@ -3,9 +3,8 @@ import { useRouter } from "next/router"; // ui import { Breadcrumbs, Button, DiceIcon } from "@plane/ui"; // components -import { BreadcrumbLink } from "@/components/common"; +import { BreadcrumbLink, Logo } from "@/components/common"; import { ModuleViewHeader } from "@/components/modules"; -import { ProjectLogo } from "@/components/project"; // constants import { EUserProjectRoles } from "@/constants/project"; // hooks @@ -41,7 +40,7 @@ export const ModulesListHeader: React.FC = observer(() => { icon={ currentProjectDetails && ( - + ) } diff --git a/web/components/headers/page-details.tsx b/web/components/headers/page-details.tsx index 3e54243056c..94ecd99574d 100644 --- a/web/components/headers/page-details.tsx +++ b/web/components/headers/page-details.tsx @@ -1,22 +1,52 @@ +import { useState } from "react"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; import { FileText } from "lucide-react"; +// types +import { TLogoProps } from "@plane/types"; // ui -import { Breadcrumbs, Button } from "@plane/ui"; +import { Breadcrumbs, Button, EmojiIconPicker, EmojiIconPickerTypes, TOAST_TYPE, setToast } from "@plane/ui"; // components -import { BreadcrumbLink } from "@/components/common"; -import { ProjectLogo } from "@/components/project"; +import { BreadcrumbLink, Logo } from "@/components/common"; +// helper +import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; // hooks import { usePage, useProject } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; +export interface IPagesHeaderProps { + showButton?: boolean; +} + export const PageDetailsHeader = observer(() => { // router const router = useRouter(); const { workspaceSlug, pageId } = router.query; + // state + const [isOpen, setIsOpen] = useState(false); // store hooks const { currentProjectDetails } = useProject(); - const { isContentEditable, isSubmitting, name } = usePage(pageId?.toString() ?? ""); + const { isContentEditable, isSubmitting, name, logo_props, updatePageLogo } = usePage(pageId?.toString() ?? ""); + + const handlePageLogoUpdate = async (data: TLogoProps) => { + if (data) { + updatePageLogo(data) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Logo Updated successfully.", + }); + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Something went wrong. Please try again.", + }); + }); + } + }; // use platform const { platform } = usePlatformOS(); // derived values @@ -38,7 +68,7 @@ export const PageDetailsHeader = observer(() => { icon={ currentProjectDetails && ( - + ) } @@ -67,7 +97,49 @@ export const PageDetailsHeader = observer(() => { } /> + setIsOpen(val)} + className="flex items-center justify-center" + buttonClassName="flex items-center justify-center" + label={ + <> + {logo_props?.in_use ? ( + + ) : ( + + )} + + } + 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; + + handlePageLogoUpdate({ + in_use: val?.type, + [val?.type]: logoValue, + }).finally(() => setIsOpen(false)); + }} + defaultIconColor={ + logo_props?.in_use && logo_props.in_use === "icon" ? logo_props?.icon?.color : undefined + } + defaultOpen={ + logo_props?.in_use && logo_props?.in_use === "emoji" + ? EmojiIconPickerTypes.EMOJI + : EmojiIconPickerTypes.ICON + } + /> + } + /> } /> diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx index 7ab9cb75d1a..b2914688c38 100644 --- a/web/components/headers/pages.tsx +++ b/web/components/headers/pages.tsx @@ -5,8 +5,7 @@ import { FileText } from "lucide-react"; // ui import { Breadcrumbs, Button } from "@plane/ui"; // helpers -import { BreadcrumbLink } from "@/components/common"; -import { ProjectLogo } from "@/components/project"; +import { BreadcrumbLink, Logo } from "@/components/common"; import { EUserProjectRoles } from "@/constants/project"; // constants // components @@ -41,7 +40,7 @@ export const PagesHeader = observer(() => { icon={ currentProjectDetails && ( - + ) } diff --git a/web/components/headers/project-archived-issue-details.tsx b/web/components/headers/project-archived-issue-details.tsx index e32528e821b..c874745a4bc 100644 --- a/web/components/headers/project-archived-issue-details.tsx +++ b/web/components/headers/project-archived-issue-details.tsx @@ -4,8 +4,7 @@ import { useRouter } from "next/router"; import useSWR from "swr"; // hooks import { ArchiveIcon, Breadcrumbs, LayersIcon } from "@plane/ui"; -import { BreadcrumbLink } from "@/components/common"; -import { ProjectLogo } from "@/components/project"; +import { BreadcrumbLink, Logo } from "@/components/common"; import { ISSUE_DETAILS } from "@/constants/fetch-keys"; import { useProject } from "@/hooks/store"; // components @@ -52,7 +51,7 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => { icon={ currentProjectDetails && ( - + ) } diff --git a/web/components/headers/project-archives.tsx b/web/components/headers/project-archives.tsx index 6e5638c7144..5022414613d 100644 --- a/web/components/headers/project-archives.tsx +++ b/web/components/headers/project-archives.tsx @@ -4,8 +4,7 @@ import { useRouter } from "next/router"; // ui import { ArchiveIcon, Breadcrumbs, Tooltip } from "@plane/ui"; // components -import { BreadcrumbLink } from "@/components/common"; -import { ProjectLogo } from "@/components/project"; +import { BreadcrumbLink, Logo } from "@/components/common"; // constants import { PROJECT_ARCHIVES_BREADCRUMB_LIST } from "@/constants/archives"; import { EIssuesStoreType } from "@/constants/issue"; @@ -49,7 +48,7 @@ export const ProjectArchivesHeader: FC = observer(() => { icon={ currentProjectDetails && ( - + ) } diff --git a/web/components/headers/project-draft-issues.tsx b/web/components/headers/project-draft-issues.tsx index f6de97b5240..8c8a25c9efd 100644 --- a/web/components/headers/project-draft-issues.tsx +++ b/web/components/headers/project-draft-issues.tsx @@ -6,9 +6,8 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption // ui import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui"; // components -import { BreadcrumbLink } from "@/components/common"; +import { BreadcrumbLink, Logo } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; -import { ProjectLogo } from "@/components/project"; // constants import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; // helpers @@ -101,7 +100,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => { icon={ currentProjectDetails && ( - + ) } diff --git a/web/components/headers/project-inbox.tsx b/web/components/headers/project-inbox.tsx index 082720358c8..ce76f3e4066 100644 --- a/web/components/headers/project-inbox.tsx +++ b/web/components/headers/project-inbox.tsx @@ -5,9 +5,8 @@ import { RefreshCcw } from "lucide-react"; // ui import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; // components -import { BreadcrumbLink } from "@/components/common"; +import { BreadcrumbLink, Logo } from "@/components/common"; import { InboxIssueCreateEditModalRoot } from "@/components/inbox"; -import { ProjectLogo } from "@/components/project"; // hooks import { useProject, useProjectInbox } from "@/hooks/store"; @@ -35,7 +34,7 @@ export const ProjectInboxHeader: FC = observer(() => { icon={ currentProjectDetails && ( - + ) } diff --git a/web/components/headers/project-issue-details.tsx b/web/components/headers/project-issue-details.tsx index 176732ca5d8..890bd59e50d 100644 --- a/web/components/headers/project-issue-details.tsx +++ b/web/components/headers/project-issue-details.tsx @@ -4,8 +4,7 @@ import { useRouter } from "next/router"; // hooks import { PanelRight } from "lucide-react"; import { Breadcrumbs, LayersIcon } from "@plane/ui"; -import { BreadcrumbLink } from "@/components/common"; -import { ProjectLogo } from "@/components/project"; +import { BreadcrumbLink, Logo } from "@/components/common"; import { cn } from "@/helpers/common.helper"; import { useAppTheme, useIssueDetail, useProject } from "@/hooks/store"; // ui @@ -42,7 +41,7 @@ export const ProjectIssueDetailsHeader: FC = observer(() => { icon={ currentProjectDetails && ( - + ) } diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 8ba44719e93..7042d2c28d6 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -9,9 +9,8 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui"; // components import { ProjectAnalyticsModal } from "@/components/analytics"; -import { BreadcrumbLink } from "@/components/common"; +import { BreadcrumbLink, Logo } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; -import { ProjectLogo } from "@/components/project"; // constants import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; import { EUserProjectRoles } from "@/constants/project"; @@ -130,7 +129,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => { currentProjectDetails ? ( currentProjectDetails && ( - + ) ) : ( diff --git a/web/components/headers/project-settings.tsx b/web/components/headers/project-settings.tsx index 36b9cd2472a..2fe48969d42 100644 --- a/web/components/headers/project-settings.tsx +++ b/web/components/headers/project-settings.tsx @@ -5,8 +5,7 @@ import { useRouter } from "next/router"; import { Settings } from "lucide-react"; import { Breadcrumbs, CustomMenu } from "@plane/ui"; // components -import { BreadcrumbLink } from "@/components/common"; -import { ProjectLogo } from "@/components/project"; +import { BreadcrumbLink, Logo } from "@/components/common"; // constants import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "@/constants/project"; // hooks @@ -39,7 +38,7 @@ export const ProjectSettingHeader: FC = observer(() => { icon={ currentProjectDetails && ( - + ) } diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index 297c976eeab..0e8f59e6c3d 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -7,9 +7,8 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption // ui import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui"; // components -import { BreadcrumbLink } from "@/components/common"; +import { BreadcrumbLink, Logo } from "@/components/common"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; -import { ProjectLogo } from "@/components/project"; // constants import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; import { EUserProjectRoles } from "@/constants/project"; @@ -141,7 +140,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { icon={ currentProjectDetails && ( - + ) } @@ -164,7 +163,11 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { - + {viewDetails?.logo_props?.in_use ? ( + + ) : ( + + )} {viewDetails?.name && truncateText(viewDetails.name, 40)} } @@ -182,7 +185,11 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { href={`/${workspaceSlug}/projects/${projectId}/views/${viewId}`} className="flex items-center gap-1.5" > - + {view?.logo_props?.in_use ? ( + + ) : ( + + )} {truncateText(view.name, 40)} diff --git a/web/components/headers/project-views.tsx b/web/components/headers/project-views.tsx index 3cd5788470d..7f1d1a725eb 100644 --- a/web/components/headers/project-views.tsx +++ b/web/components/headers/project-views.tsx @@ -1,14 +1,13 @@ import { observer } from "mobx-react"; import { useRouter } from "next/router"; -// hooks -// components +// ui import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui"; -import { BreadcrumbLink } from "@/components/common"; -// helpers -import { ProjectLogo } from "@/components/project"; +// components +import { BreadcrumbLink, Logo } from "@/components/common"; import { ViewListHeader } from "@/components/views"; -import { EUserProjectRoles } from "@/constants/project"; // constants +import { EUserProjectRoles } from "@/constants/project"; +// hooks import { useCommandPalette, useProject, useUser } from "@/hooks/store"; export const ProjectViewsHeader: React.FC = observer(() => { @@ -40,7 +39,7 @@ export const ProjectViewsHeader: React.FC = observer(() => { icon={ currentProjectDetails && ( - + ) } diff --git a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx index 54dc039191b..190d9f1fa15 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/project.tsx @@ -1,9 +1,9 @@ import { observer } from "mobx-react"; import { X } from "lucide-react"; +// components +import { Logo } from "@/components/common"; // hooks -import { ProjectLogo } from "@/components/project"; import { useProject } from "@/hooks/store"; -// components type Props = { handleRemove: (val: string) => void; @@ -26,7 +26,7 @@ export const AppliedProjectFilters: React.FC = observer((props) => { return (
- + {projectDetails.name} {editable && ( diff --git a/web/components/issues/issue-layouts/filters/header/filters/project.tsx b/web/components/issues/issue-layouts/filters/header/filters/project.tsx index 26b0bb46bae..d739674813e 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/project.tsx @@ -1,15 +1,13 @@ import React, { useMemo, useState } from "react"; import sortBy from "lodash/sortBy"; import { observer } from "mobx-react"; -// components +// ui import { Loader } from "@plane/ui"; +// components +import { Logo } from "@/components/common"; import { FilterHeader, FilterOption } from "@/components/issues"; // hooks -import { ProjectLogo } from "@/components/project"; import { useProject } from "@/hooks/store"; -// components -// ui -// helpers type Props = { appliedFilters: string[] | null; @@ -65,7 +63,7 @@ export const FilterProjects: React.FC = observer((props) => { onClick={() => handleUpdate(project.id)} icon={ - + } title={project.name} diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx index 2b12244a462..78048b4b4ba 100644 --- a/web/components/issues/issue-layouts/utils.tsx +++ b/web/components/issues/issue-layouts/utils.tsx @@ -5,6 +5,7 @@ import pull from "lodash/pull"; import uniq from "lodash/uniq"; import scrollIntoView from "smooth-scroll-into-view-if-needed"; import { ContrastIcon } from "lucide-react"; +// types import { GroupByColumnTypes, IGroupByColumn, @@ -13,12 +14,14 @@ import { TIssue, TIssueGroupByOptions, } from "@plane/types"; +// ui import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui"; // components -import { ProjectLogo } from "@/components/project"; -// stores +import { Logo } from "@/components/common"; +// constants import { ISSUE_PRIORITIES, EIssuesStoreType } from "@/constants/issue"; import { STATE_GROUPS } from "@/constants/state"; +// stores import { ICycleStore } from "@/store/cycle.store"; import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store"; import { ILabelStore } from "@/store/label.store"; @@ -26,9 +29,6 @@ import { IMemberRootStore } from "@/store/member"; import { IModuleStore } from "@/store/module.store"; import { IProjectStore } from "@/store/project/project.store"; import { IStateStore } from "@/store/state.store"; -// helpers -// constants -// types export const HIGHLIGHT_CLASS = "highlight"; export const HIGHLIGHT_WITH_LINE = "highlight-with-line"; @@ -101,7 +101,7 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined name: project.name, icon: (
- +
), payload: { project_id: project.id }, diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index b745921125d..37b8856ef2d 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -59,13 +59,17 @@ export const ModuleListItem: React.FC = observer((props) => { } }; + const handleArchivedModuleClick = (e: React.MouseEvent) => { + openModuleOverview(e); + }; + + const handleItemClick = moduleDetails.archived_at ? handleArchivedModuleClick : undefined; + return ( { - if (moduleDetails.archived_at) openModuleOverview(e); - }} + onItemClick={handleItemClick} prependTitleElement={ {completedModuleCheck ? ( diff --git a/web/components/pages/list/block.tsx b/web/components/pages/list/block.tsx index d46d07e7428..40c2a5fafb6 100644 --- a/web/components/pages/list/block.tsx +++ b/web/components/pages/list/block.tsx @@ -1,9 +1,16 @@ -import { FC, useRef } from "react"; +import { FC, useRef, useState } from "react"; import { observer } from "mobx-react"; +import { FileText } from "lucide-react"; +// types +import { TLogoProps } from "@plane/types"; +// ui +import { EmojiIconPicker, EmojiIconPickerTypes, TOAST_TYPE, setToast } from "@plane/ui"; // components +import { Logo } from "@/components/common"; import { ListItem } from "@/components/core/list"; import { BlockItemAction } from "@/components/pages/list"; // helpers +import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; import { getPageName } from "@/helpers/page.helper"; // hooks import { usePage } from "@/hooks/store"; @@ -19,12 +26,74 @@ export const PageListBlock: FC = observer((props) => { const { workspaceSlug, projectId, pageId } = props; // refs const parentRef = useRef(null); + // state + const [isOpen, setIsOpen] = useState(false); // hooks - const { name } = usePage(pageId); + const { name, logo_props, updatePageLogo } = usePage(pageId); const { isMobile } = usePlatformOS(); + const handlePageLogoUpdate = async (data: TLogoProps) => { + if (data) { + updatePageLogo(data) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Logo Updated successfully.", + }); + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Something went wrong. Please try again.", + }); + }); + } + }; + return ( + setIsOpen(val)} + className="flex items-center justify-center" + buttonClassName="flex items-center justify-center" + label={ + <> + {logo_props?.in_use ? ( + + ) : ( + + )} + + } + 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; + + handlePageLogoUpdate({ + in_use: val?.type, + [val?.type]: logoValue, + }).finally(() => setIsOpen(false)); + }} + defaultIconColor={logo_props?.in_use && logo_props.in_use === "icon" ? logo_props?.icon?.color : undefined} + defaultOpen={ + logo_props?.in_use && logo_props?.in_use === "emoji" + ? EmojiIconPickerTypes.EMOJI + : EmojiIconPickerTypes.ICON + } + /> + + } title={getPageName(name)} itemLink={`/${workspaceSlug}/projects/${projectId}/pages/${pageId}`} actionableItems={ @@ -32,6 +101,7 @@ export const PageListBlock: FC = observer((props) => { } isMobile={isMobile} parentRef={parentRef} + disableLink={isOpen} /> ); }); diff --git a/web/components/pages/modals/create-page-modal.tsx b/web/components/pages/modals/create-page-modal.tsx index cbc42437f65..ea3f4473716 100644 --- a/web/components/pages/modals/create-page-modal.tsx +++ b/web/components/pages/modals/create-page-modal.tsx @@ -26,6 +26,7 @@ export const CreatePageModal: FC = (props) => { id: undefined, name: "", access: EPageAccess.PUBLIC, + logo_props: undefined, }); // router const router = useRouter(); diff --git a/web/components/pages/modals/page-form.tsx b/web/components/pages/modals/page-form.tsx index 36b470bb8d0..a300f9f2b0b 100644 --- a/web/components/pages/modals/page-form.tsx +++ b/web/components/pages/modals/page-form.tsx @@ -1,12 +1,15 @@ import { FormEvent, useState } from "react"; // types +import { FileText } from "lucide-react"; import { TPage } from "@plane/types"; // ui -import { Button, Input, Tooltip } from "@plane/ui"; +import { Button, EmojiIconPicker, EmojiIconPickerTypes, Input, Tooltip } from "@plane/ui"; +import { Logo } from "@/components/common"; // constants import { PAGE_ACCESS_SPECIFIERS } from "@/constants/page"; // helpers import { cn } from "@/helpers/common.helper"; +import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -22,6 +25,7 @@ export const PageForm: React.FC = (props) => { // hooks const { isMobile } = usePlatformOS(); // state + const [isOpen, setIsOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const handlePageFormSubmit = async (e: FormEvent) => { @@ -41,21 +45,66 @@ export const PageForm: React.FC = (props) => {

Create Page

-
- handleFormData("name", e.target.value)} - placeholder="Title" - className="w-full resize-none text-base" - tabIndex={1} - required - autoFocus +
+ setIsOpen(val)} + className="flex items-center justify-center flex-shrink0" + buttonClassName="flex items-center justify-center" + label={ + + <> + {formData?.logo_props?.in_use ? ( + + ) : ( + + )} + + + } + 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; + + handleFormData("logo_props", { + in_use: val?.type, + [val?.type]: logoValue, + }); + setIsOpen(false); + }} + defaultIconColor={ + formData?.logo_props?.in_use && formData?.logo_props?.in_use === "icon" + ? formData?.logo_props?.icon?.color + : undefined + } + defaultOpen={ + formData?.logo_props?.in_use && formData?.logo_props?.in_use === "emoji" + ? EmojiIconPickerTypes.EMOJI + : EmojiIconPickerTypes.ICON + } /> - {isTitleLengthMoreThan255Character && ( - Max length of the name should be less than 255 characters - )} +
+ handleFormData("name", e.target.value)} + placeholder="Title" + className="w-full resize-none text-base" + tabIndex={1} + required + autoFocus + /> + {isTitleLengthMoreThan255Character && ( + Max length of the name should be less than 255 characters + )} +
diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index c7e68ea79c3..75a1b2c015e 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -5,13 +5,13 @@ import { useRouter } from "next/router"; import useSWR from "swr"; // icons import { ChevronDown, Pencil } from "lucide-react"; -// ui +// headless ui import { Disclosure, Transition } from "@headlessui/react"; -// icons // plane ui import { Loader, Tooltip } from "@plane/ui"; +// components +import { Logo } from "@/components/common"; // fetch-keys -import { ProjectLogo } from "@/components/project"; import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys"; // helpers import { renderFormattedDate } from "@/helpers/date-time.helper"; @@ -151,7 +151,7 @@ export const ProfileSidebar = observer(() => {
- +
{projectDetails.name}
diff --git a/web/components/project/card.tsx b/web/components/project/card.tsx index 8f29b245b0e..8f94c6bca3e 100644 --- a/web/components/project/card.tsx +++ b/web/components/project/card.tsx @@ -18,8 +18,9 @@ import { TContextMenuItem, } from "@plane/ui"; // components +import { Logo } from "@/components/common"; import { FavoriteStar } from "@/components/core"; -import { ArchiveRestoreProjectModal, DeleteProjectModal, JoinProjectModal, ProjectLogo } from "@/components/project"; +import { ArchiveRestoreProjectModal, DeleteProjectModal, JoinProjectModal } from "@/components/project"; // constants import { EUserProjectRoles } from "@/constants/project"; // helpers @@ -203,7 +204,7 @@ export const ProjectCard: React.FC = observer((props) => {
- +
diff --git a/web/components/project/create-project-form.tsx b/web/components/project/create-project-form.tsx index 7fc8419fe78..7634432ca3e 100644 --- a/web/components/project/create-project-form.tsx +++ b/web/components/project/create-project-form.tsx @@ -16,6 +16,7 @@ import { Tooltip, } from "@plane/ui"; // components +import { Logo } from "@/components/common"; import { ImagePickerPopover } from "@/components/core"; import { MemberDropdown } from "@/components/dropdowns"; // constants @@ -28,8 +29,6 @@ import { projectIdentifierSanitizer } from "@/helpers/project.helper"; // hooks import { useEventTracker, useProject } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; -// types -import { ProjectLogo } from "./project-logo"; type Props = { setToFavorite?: boolean; @@ -59,6 +58,7 @@ export const CreateProjectForm: FC = observer((props) => { const { captureProjectEvent } = useEventTracker(); const { addProjectToFavorites, createProject } = useProject(); // states + const [isOpen, setIsOpen] = useState(false); const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); // form info const { @@ -189,9 +189,13 @@ export const CreateProjectForm: FC = observer((props) => { control={control} render={({ field: { value, onChange } }) => ( setIsOpen(val)} + className="flex items-center justify-center" + buttonClassName="flex items-center justify-center" label={ - + } onChange={(val: any) => { @@ -208,6 +212,7 @@ export const CreateProjectForm: FC = observer((props) => { in_use: val?.type, [val?.type]: logoValue, }); + setIsOpen(false); }} defaultIconColor={value.in_use && value.in_use === "icon" ? value.icon?.color : undefined} defaultOpen={ diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx index f617910f22d..547aaa61c8d 100644 --- a/web/components/project/form.tsx +++ b/web/components/project/form.tsx @@ -16,6 +16,7 @@ import { Tooltip, } from "@plane/ui"; // components +import { Logo } from "@/components/common"; import { ImagePickerPopover } from "@/components/core"; // constants import { PROJECT_UPDATED } from "@/constants/event-tracker"; @@ -29,7 +30,6 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; // services import { ProjectService } from "@/services/project"; // types -import { ProjectLogo } from "./project-logo"; export interface IProjectDetailsForm { project: IProject; workspaceSlug: string; @@ -40,6 +40,7 @@ const projectService = new ProjectService(); export const ProjectDetailsForm: FC = (props) => { const { project, workspaceSlug, projectId, isAdmin } = props; // states + const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); // store hooks const { captureProjectEvent } = useEventTracker(); @@ -149,11 +150,11 @@ export const ProjectDetailsForm: FC = (props) => { name="logo_props" render={({ field: { value, onChange } }) => ( - - - } + isOpen={isOpen} + handleToggle={(val: boolean) => setIsOpen(val)} + className="flex items-center justify-center" + buttonClassName="flex items-center justify-center" + label={} onChange={(val) => { let logoValue = {}; @@ -168,6 +169,7 @@ export const ProjectDetailsForm: FC = (props) => { in_use: val?.type, [val?.type]: logoValue, }); + setIsOpen(false); }} defaultIconColor={value?.in_use && value.in_use === "icon" ? value?.icon?.color : undefined} defaultOpen={ diff --git a/web/components/project/index.ts b/web/components/project/index.ts index db51bc28401..3c0c337d3d7 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -18,7 +18,6 @@ export * from "./sidebar-list"; export * from "./integration-card"; export * from "./member-list"; export * from "./member-list-item"; -export * from "./project-logo"; export * from "./project-settings-member-defaults"; export * from "./send-project-invitation-modal"; export * from "./confirm-project-member-remove"; diff --git a/web/components/project/project-feature-update.tsx b/web/components/project/project-feature-update.tsx index 2359b660961..24c7091eded 100644 --- a/web/components/project/project-feature-update.tsx +++ b/web/components/project/project-feature-update.tsx @@ -1,13 +1,13 @@ import React, { FC } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; -// hooks -import { Button, getButtonStyling } from "@plane/ui"; -import { useProject } from "@/hooks/store"; // ui +import { Button, getButtonStyling } from "@plane/ui"; // components -import { ProjectLogo } from "./project-logo"; -import { ProjectFeaturesList } from "./settings"; +import { Logo } from "@/components/common"; +import { ProjectFeaturesList } from "@/components/project/settings"; +// hooks +import { useProject } from "@/hooks/store"; type Props = { workspaceSlug: string; @@ -35,7 +35,7 @@ export const ProjectFeatureUpdate: FC = observer((props) => {
- Congrats! Project {" "} + Congrats! Project {" "}

{currentProjectDetails.name}

created.
diff --git a/web/components/project/project-logo.tsx b/web/components/project/project-logo.tsx deleted file mode 100644 index fc90fdba3d1..00000000000 --- a/web/components/project/project-logo.tsx +++ /dev/null @@ -1,34 +0,0 @@ -// helpers -import { TProjectLogoProps } from "@plane/types"; -import { cn } from "@/helpers/common.helper"; -// types - -type Props = { - className?: string; - logo: TProjectLogoProps; -}; - -export const ProjectLogo: React.FC = (props) => { - const { className, logo } = props; - - if (logo?.in_use === "icon" && logo?.icon) - return ( - - {logo.icon.name} - - ); - - if (logo?.in_use === "emoji" && logo?.emoji) - return ( - - {logo.emoji.value?.split("-").map((emoji) => String.fromCodePoint(parseInt(emoji, 10)))} - - ); - - return <>; -}; diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index 052460c538f..bf8e823d6d7 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -8,6 +8,7 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { useRouter } from "next/router"; import { createRoot } from "react-dom/client"; +// icons import { MoreVertical, PenSquare, @@ -21,8 +22,8 @@ import { MoreHorizontal, Inbox, } from "lucide-react"; +// headless ui import { Disclosure, Transition } from "@headlessui/react"; -// icons // ui import { CustomMenu, @@ -35,8 +36,12 @@ import { setPromiseToast, DropIndicator, } from "@plane/ui"; -import { LeaveProjectModal, ProjectLogo, PublishProjectModal } from "@/components/project"; +// components +import { Logo } from "@/components/common"; +import { LeaveProjectModal, PublishProjectModal } from "@/components/project"; +// constants import { EUserProjectRoles } from "@/constants/project"; +// helpers import { cn } from "@/helpers/common.helper"; // hooks import { useAppTheme, useEventTracker, useProject } from "@/hooks/store"; @@ -203,8 +208,8 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { const root = createRoot(container); root.render(
-
- {project && } +
+ {project && }

{project?.name}

@@ -331,8 +336,8 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { "justify-center": isCollapsed, })} > -
- +
+
{!isCollapsed &&

{project.name}

}
diff --git a/web/components/views/form.tsx b/web/components/views/form.tsx index a8d9ea7553f..b0786ec1b5f 100644 --- a/web/components/views/form.tsx +++ b/web/components/views/form.tsx @@ -1,14 +1,17 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; // types import { IProjectView, IIssueFilterOptions } from "@plane/types"; // ui -import { Button, Input, TextArea } from "@plane/ui"; +import { Button, EmojiIconPicker, EmojiIconPickerTypes, Input, PhotoFilterIcon, TextArea } from "@plane/ui"; // components +import { Logo } from "@/components/common"; import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/components/issues"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; +// helpers +import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; // hooks import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store"; @@ -26,6 +29,8 @@ const defaultValues: Partial = { export const ProjectViewForm: React.FC = observer((props) => { const { handleFormSubmit, handleClose, data, preLoadedData } = props; + // state + const [isOpen, setIsOpen] = useState(false); // store hooks const { currentProjectDetails } = useProject(); const { projectStates } = useProjectState(); @@ -45,6 +50,8 @@ export const ProjectViewForm: React.FC = observer((props) => { defaultValues, }); + const logoValue = watch("logo_props"); + const selectedFilters: IIssueFilterOptions = {}; Object.entries(watch("filters") ?? {}).forEach(([key, value]) => { if (!value) return; @@ -85,6 +92,7 @@ export const ProjectViewForm: React.FC = observer((props) => { await handleFormSubmit({ name: formData.name, description: formData.description, + logo_props: formData.logo_props, filters: formData.filters, } as IProjectView); @@ -112,33 +120,74 @@ export const ProjectViewForm: React.FC = observer((props) => {

{data ? "Update" : "Create"} View

-
- + setIsOpen(val)} + className="flex items-center justify-center flex-shrink0" + buttonClassName="flex items-center justify-center" + label={ + + <> + {logoValue?.in_use ? ( + + ) : ( + + )} + + + } + 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; + + setValue("logo_props", { + in_use: val?.type, + [val?.type]: logoValue, + }); + setIsOpen(false); }} - render={({ field: { value, onChange } }) => ( - - )} + defaultIconColor={logoValue?.in_use && logoValue?.in_use === "icon" ? logoValue?.icon?.color : undefined} + defaultOpen={ + logoValue?.in_use && logoValue?.in_use === "emoji" + ? EmojiIconPickerTypes.EMOJI + : EmojiIconPickerTypes.ICON + } /> - {errors?.name?.message} +
+ ( + + )} + /> + {errors?.name?.message} +
= observer((props) => { const { view } = props; // refs const parentRef = useRef(null); + // state + const [isOpen, setIsOpen] = useState(false); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; // store hooks const { isMobile } = usePlatformOS(); + const { updateView } = useProjectView(); + + const handleViewLogoUpdate = async (data: TLogoProps) => { + if (!workspaceSlug || !projectId || !view.id || !data) return; + + updateView(workspaceSlug.toString(), projectId.toString(), view.id.toString(), { + logo_props: data, + }) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Logo Updated successfully.", + }); + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Something went wrong. Please try again.", + }); + }); + }; return ( + setIsOpen(val)} + className="flex items-center justify-center" + buttonClassName="flex items-center justify-center" + label={ + <> + {view?.logo_props?.in_use ? ( + + ) : ( + + )} + + } + 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; + + handleViewLogoUpdate({ + in_use: val?.type, + [val?.type]: logoValue, + }).finally(() => setIsOpen(false)); + }} + defaultIconColor={ + view?.logo_props?.in_use && view?.logo_props.in_use === "icon" ? view?.logo_props?.icon?.color : undefined + } + defaultOpen={ + view?.logo_props?.in_use && view?.logo_props?.in_use === "emoji" + ? EmojiIconPickerTypes.EMOJI + : EmojiIconPickerTypes.ICON + } + /> + + } title={view.name} itemLink={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`} actionableItems={} isMobile={isMobile} parentRef={parentRef} + disableLink={isOpen} /> ); }); diff --git a/web/helpers/emoji.helper.tsx b/web/helpers/emoji.helper.tsx index 513f9b6c4e1..72f22bed5c4 100644 --- a/web/helpers/emoji.helper.tsx +++ b/web/helpers/emoji.helper.tsx @@ -63,3 +63,16 @@ export const convertHexEmojiToDecimal = (emojiUnified: string): string => { .map((e) => parseInt(e, 16)) .join("-"); }; + + +export const emojiCodeToUnicode = (emoji: string) => { + if (!emoji) return ""; + + // convert emoji code to unicode + const uniCodeEmoji = emoji + .split("-") + .map((emoji) => parseInt(emoji, 10).toString(16)) + .join("-"); + + return uniCodeEmoji; +}; diff --git a/web/store/pages/page.store.ts b/web/store/pages/page.store.ts index 2897056d5ce..9c1d6649f56 100644 --- a/web/store/pages/page.store.ts +++ b/web/store/pages/page.store.ts @@ -1,7 +1,7 @@ import set from "lodash/set"; import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx"; // types -import { TPage } from "@plane/types"; +import { TLogoProps, TPage } from "@plane/types"; // constants import { EPageAccess } from "@/constants/page"; import { EUserProjectRoles } from "@/constants/project"; @@ -38,6 +38,7 @@ export interface IPageStore extends TPage { unlock: () => Promise; archive: () => Promise; restore: () => Promise; + updatePageLogo: (logo_props: TLogoProps) => Promise; addToFavorites: () => Promise; removeFromFavorites: () => Promise; } @@ -48,6 +49,7 @@ export class PageStore implements IPageStore { // page properties id: string | undefined; name: string | undefined; + logo_props: TLogoProps | undefined; description_html: string | undefined; color: string | undefined; labels: string[] | undefined; @@ -75,6 +77,7 @@ export class PageStore implements IPageStore { ) { this.id = page?.id || undefined; this.name = page?.name; + this.logo_props = page?.logo_props || undefined; this.description_html = page?.description_html || undefined; this.color = page?.color || undefined; this.labels = page?.labels || undefined; @@ -97,6 +100,7 @@ export class PageStore implements IPageStore { // page properties id: observable.ref, name: observable.ref, + logo_props: observable.ref, description_html: observable.ref, color: observable.ref, labels: observable, @@ -135,6 +139,7 @@ export class PageStore implements IPageStore { unlock: action, archive: action, restore: action, + updatePageLogo: action, addToFavorites: action, removeFromFavorites: action, }); @@ -178,6 +183,7 @@ export class PageStore implements IPageStore { labels: this.labels, owned_by: this.owned_by, access: this.access, + logo_props: this.logo_props, is_favorite: this.is_favorite, is_locked: this.is_locked, archived_at: this.archived_at, @@ -455,6 +461,22 @@ export class PageStore implements IPageStore { } }; + updatePageLogo = async (logo_props: TLogoProps) => { + const { workspaceSlug, projectId } = this.store.router; + if (!workspaceSlug || !projectId || !this.id) return undefined; + + try { + await this.pageService.update(workspaceSlug, projectId, this.id, { + logo_props, + }); + runInAction(() => { + this.logo_props = logo_props; + }); + } catch (error) { + throw error; + } + }; + /** * @description add the page to favorites */