[WEB-393] feat: new emoji picker using emoji-picker-react (#3868)

* chore: emoji-picker-react package added

* chore: emoji and emoji picker component added

* chore: emoji picker custom style added

* chore: migration of the emoji's

* chore: migration changes

* chore: project logo prop

* chore: added logo props in the serializer

* chore: removed unused keys

* chore: implement emoji picker throughout the web app

* style: emoji icon picker

* chore: update project logo renderer in the space app

* chore: migrations fixes

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
Aaryan Khandelwal 2024-03-06 19:15:48 +05:30 committed by GitHub
parent b3d3c0fb06
commit e4f48d6878
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 1513 additions and 2462 deletions

View File

@ -95,8 +95,7 @@ class ProjectLiteSerializer(BaseSerializer):
"identifier", "identifier",
"name", "name",
"cover_image", "cover_image",
"icon_prop", "logo_props",
"emoji",
"description", "description",
] ]
read_only_fields = fields read_only_fields = fields

View File

@ -1366,10 +1366,6 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
) )
.values( .values(
"id", "id",
"name",
"identifier",
"emoji",
"icon_prop",
"created_issues", "created_issues",
"assigned_issues", "assigned_issues",
"completed_issues", "completed_issues",

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.7 on 2024-03-01 07:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0060_cycle_progress_snapshot'),
]
operations = [
migrations.AlterField(
model_name='issuelink',
name='url',
field=models.TextField(),
),
]

View File

@ -0,0 +1,54 @@
# Generated by Django 4.2.7 on 2024-03-03 16:25
from django.db import migrations, models
class Migration(migrations.Migration):
def update_project_logo_props(apps, schema_editor):
Project = apps.get_model("db", "Project")
bulk_update_project_logo = []
# Iterate through projects and update logo_props
for project in Project.objects.all():
project.logo_props["in_use"] = "emoji" if project.emoji else "icon"
project.logo_props["emoji"] = {
"value": project.emoji if project.emoji else "",
"url": "",
}
project.logo_props["icon"] = {
"name": (
project.icon_prop.get("name", "")
if project.icon_prop
else ""
),
"color": (
project.icon_prop.get("color", "")
if project.icon_prop
else ""
),
}
bulk_update_project_logo.append(project)
# Bulk update logo_props for all projects
Project.objects.bulk_update(
bulk_update_project_logo, ["logo_props"], batch_size=1000
)
dependencies = [
("db", "0060_cycle_progress_snapshot"),
]
operations = [
migrations.AlterField(
model_name="issuelink",
name="url",
field=models.TextField(),
),
migrations.AddField(
model_name="project",
name="logo_props",
field=models.JSONField(default=dict),
),
migrations.RunPython(update_project_logo_props),
]

View File

@ -107,6 +107,7 @@ class Project(BaseModel):
close_in = models.IntegerField( close_in = models.IntegerField(
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)] default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
) )
logo_props = models.JSONField(default=dict)
default_state = models.ForeignKey( default_state = models.ForeignKey(
"db.State", "db.State",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,

View File

@ -1,12 +1,26 @@
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import type { import type {
IProjectViewProps,
IUser, IUser,
IUserLite, IUserLite,
IUserMemberLite,
IWorkspace, IWorkspace,
IWorkspaceLite, IWorkspaceLite,
TStateGroups, TStateGroups,
} from "."; } from ".";
export type TProjectLogoProps = {
in_use: "emoji" | "icon";
emoji?: {
value?: string;
url?: string;
};
icon?: {
name?: string;
color?: string;
};
};
export interface IProject { export interface IProject {
archive_in: number; archive_in: number;
close_in: number; close_in: number;
@ -21,24 +35,13 @@ export interface IProject {
default_assignee: IUser | string | null; default_assignee: IUser | string | null;
default_state: string | null; default_state: string | null;
description: string; description: string;
emoji: string | null;
emoji_and_icon:
| string
| {
name: string;
color: string;
}
| null;
estimate: string | null; estimate: string | null;
icon_prop: {
name: string;
color: string;
} | null;
id: string; id: string;
identifier: string; identifier: string;
is_deployed: boolean; is_deployed: boolean;
is_favorite: boolean; is_favorite: boolean;
is_member: boolean; is_member: boolean;
logo_props: TProjectLogoProps;
member_role: EUserProjectRoles | null; member_role: EUserProjectRoles | null;
members: IProjectMemberLite[]; members: IProjectMemberLite[];
name: string; name: string;

View File

@ -132,11 +132,7 @@ export interface IUserProfileProjectSegregation {
assigned_issues: number; assigned_issues: number;
completed_issues: number; completed_issues: number;
created_issues: number; created_issues: number;
emoji: string | null;
icon_prop: null;
id: string; id: string;
identifier: string;
name: string;
pending_issues: number; pending_issues: number;
}[]; }[];
user_data: { user_data: {

View File

@ -23,6 +23,7 @@
"@headlessui/react": "^1.7.17", "@headlessui/react": "^1.7.17",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"emoji-picker-react": "^4.5.16",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-popper": "^2.3.0", "react-popper": "^2.3.0",

View File

@ -0,0 +1,169 @@
import React, { useState } from "react";
import { usePopper } from "react-popper";
import EmojiPicker, { EmojiClickData, Theme } 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",
},
];
export const CustomEmojiIconPicker: React.FC<TCustomEmojiPicker> = (props) => {
const {
buttonClassName,
className,
closeOnSelect = true,
defaultIconColor = "#5f5f5f",
defaultOpen = EmojiIconPickerTypes.EMOJI,
disabled = false,
dropdownClassName,
label,
onChange,
placement = "bottom-start",
searchPlaceholder = "Search",
theme,
} = props;
// refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// popper-js
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement,
modifiers: [
{
name: "preventOverflow",
options: {
padding: 20,
},
},
],
});
return (
<Popover as="div" className={cn("relative", className)}>
{({ close }) => (
<>
<Popover.Button as={React.Fragment}>
<button
type="button"
ref={setReferenceElement}
className={cn("outline-none", buttonClassName)}
disabled={disabled}
>
{label}
</button>
</Popover.Button>
<Popover.Panel className="fixed z-10">
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
className={cn(
"h-80 w-80 bg-custom-background-100 rounded-md border-[0.5px] border-custom-border-300 overflow-hidden",
dropdownClassName
)}
>
<Tab.Group
as="div"
className="h-full w-full flex flex-col overflow-hidden"
defaultIndex={TABS_LIST.findIndex((tab) => tab.key === defaultOpen)}
>
<Tab.List as="div" className="grid grid-cols-2 gap-1 p-2">
{TABS_LIST.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
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}
</Tab>
))}
</Tab.List>
<Tab.Panels as="div" className="h-full w-full overflow-y-auto">
<Tab.Panel>
<EmojiPicker
onEmojiClick={(val) => {
onChange({
type: EmojiIconPickerTypes.EMOJI,
value: val,
});
if (closeOnSelect) close();
}}
height="20rem"
width="100%"
theme={theme}
searchPlaceholder={searchPlaceholder}
previewConfig={{
showPreview: false,
}}
/>
</Tab.Panel>
<Tab.Panel>
<IconsList
defaultColor={defaultIconColor}
onChange={(val) => {
onChange({
type: EmojiIconPickerTypes.ICON,
value: val,
});
if (closeOnSelect) close();
}}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</Popover.Panel>
</>
)}
</Popover>
);
};

View File

@ -0,0 +1,110 @@
import React, { useEffect, useState } from "react";
// components
import { Input } from "../form-fields";
// helpers
import { cn } from "../../helpers";
// constants
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"];
export const IconsList: React.FC<TIconsListProps> = (props) => {
const { defaultColor, onChange } = props;
// states
const [activeColor, setActiveColor] = useState(defaultColor);
const [showHexInput, setShowHexInput] = useState(false);
const [hexValue, setHexValue] = useState("");
useEffect(() => {
if (DEFAULT_COLORS.includes(defaultColor.toLowerCase())) setShowHexInput(false);
else {
setHexValue(defaultColor.slice(1, 7));
setShowHexInput(true);
}
}, [defaultColor]);
return (
<>
<div className="grid grid-cols-8 gap-2 items-center justify-items-center px-2.5 h-9">
{showHexInput ? (
<div className="col-span-7 flex items-center gap-1 justify-self-stretch ml-2">
<span
className="h-4 w-4 flex-shrink-0 rounded-full mr-1"
style={{
backgroundColor: `#${hexValue}`,
}}
/>
<span className="text-xs text-custom-text-300 flex-shrink-0">HEX</span>
<span className="text-xs text-custom-text-200 flex-shrink-0 -mr-1">#</span>
<Input
type="text"
value={hexValue}
onChange={(e) => {
const value = e.target.value;
setHexValue(value);
if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(`#${value}`);
}}
className="flex-grow pl-0 text-xs text-custom-text-200"
mode="true-transparent"
autoFocus
/>
</div>
) : (
DEFAULT_COLORS.map((curCol) => (
<button
key={curCol}
type="button"
className="grid place-items-center"
onClick={() => {
setActiveColor(curCol);
setHexValue(curCol.slice(1, 7));
}}
>
<span className="h-4 w-4 cursor-pointer rounded-full" style={{ backgroundColor: curCol }} />
</button>
))
)}
<button
type="button"
className={cn("grid place-items-center h-4 w-4 rounded-full border border-transparent", {
"border-custom-border-400": !showHexInput,
})}
onClick={() => {
setShowHexInput((prevData) => !prevData);
setHexValue(activeColor.slice(1, 7));
}}
>
{showHexInput ? (
<span className="conical-gradient h-4 w-4 rounded-full" />
) : (
<span className="text-custom-text-300 text-[0.6rem] grid place-items-center">#</span>
)}
</button>
</div>
<div className="grid grid-cols-8 gap-2 px-2.5 justify-items-center mt-2">
{MATERIAL_ICONS_LIST.map((icon) => (
<button
key={icon.name}
type="button"
className="h-6 w-6 select-none text-lg grid place-items-center rounded hover:bg-custom-background-80"
onClick={() => {
onChange({
name: icon.name,
color: activeColor,
});
}}
>
<span style={{ color: activeColor }} className="material-symbols-rounded text-base">
{icon.name}
</span>
</button>
))}
</div>
</>
);
};

View File

@ -0,0 +1,605 @@
export const MATERIAL_ICONS_LIST = [
{
name: "search",
},
{
name: "home",
},
{
name: "menu",
},
{
name: "close",
},
{
name: "settings",
},
{
name: "done",
},
{
name: "check_circle",
},
{
name: "favorite",
},
{
name: "add",
},
{
name: "delete",
},
{
name: "arrow_back",
},
{
name: "star",
},
{
name: "logout",
},
{
name: "add_circle",
},
{
name: "cancel",
},
{
name: "arrow_drop_down",
},
{
name: "more_vert",
},
{
name: "check",
},
{
name: "check_box",
},
{
name: "toggle_on",
},
{
name: "open_in_new",
},
{
name: "refresh",
},
{
name: "login",
},
{
name: "radio_button_unchecked",
},
{
name: "more_horiz",
},
{
name: "apps",
},
{
name: "radio_button_checked",
},
{
name: "download",
},
{
name: "remove",
},
{
name: "toggle_off",
},
{
name: "bolt",
},
{
name: "arrow_upward",
},
{
name: "filter_list",
},
{
name: "delete_forever",
},
{
name: "autorenew",
},
{
name: "key",
},
{
name: "sort",
},
{
name: "sync",
},
{
name: "add_box",
},
{
name: "block",
},
{
name: "restart_alt",
},
{
name: "menu_open",
},
{
name: "shopping_cart_checkout",
},
{
name: "expand_circle_down",
},
{
name: "backspace",
},
{
name: "undo",
},
{
name: "done_all",
},
{
name: "do_not_disturb_on",
},
{
name: "open_in_full",
},
{
name: "double_arrow",
},
{
name: "sync_alt",
},
{
name: "zoom_in",
},
{
name: "done_outline",
},
{
name: "drag_indicator",
},
{
name: "fullscreen",
},
{
name: "star_half",
},
{
name: "settings_accessibility",
},
{
name: "reply",
},
{
name: "exit_to_app",
},
{
name: "unfold_more",
},
{
name: "library_add",
},
{
name: "cached",
},
{
name: "select_check_box",
},
{
name: "terminal",
},
{
name: "change_circle",
},
{
name: "disabled_by_default",
},
{
name: "swap_horiz",
},
{
name: "swap_vert",
},
{
name: "app_registration",
},
{
name: "download_for_offline",
},
{
name: "close_fullscreen",
},
{
name: "file_open",
},
{
name: "minimize",
},
{
name: "open_with",
},
{
name: "dataset",
},
{
name: "add_task",
},
{
name: "start",
},
{
name: "keyboard_voice",
},
{
name: "create_new_folder",
},
{
name: "forward",
},
{
name: "download",
},
{
name: "settings_applications",
},
{
name: "compare_arrows",
},
{
name: "redo",
},
{
name: "zoom_out",
},
{
name: "publish",
},
{
name: "html",
},
{
name: "token",
},
{
name: "switch_access_shortcut",
},
{
name: "fullscreen_exit",
},
{
name: "sort_by_alpha",
},
{
name: "delete_sweep",
},
{
name: "indeterminate_check_box",
},
{
name: "view_timeline",
},
{
name: "settings_backup_restore",
},
{
name: "arrow_drop_down_circle",
},
{
name: "assistant_navigation",
},
{
name: "sync_problem",
},
{
name: "clear_all",
},
{
name: "density_medium",
},
{
name: "heart_plus",
},
{
name: "filter_alt_off",
},
{
name: "expand",
},
{
name: "subdirectory_arrow_right",
},
{
name: "download_done",
},
{
name: "arrow_outward",
},
{
name: "123",
},
{
name: "swipe_left",
},
{
name: "auto_mode",
},
{
name: "saved_search",
},
{
name: "place_item",
},
{
name: "system_update_alt",
},
{
name: "javascript",
},
{
name: "search_off",
},
{
name: "output",
},
{
name: "select_all",
},
{
name: "fit_screen",
},
{
name: "swipe_up",
},
{
name: "dynamic_form",
},
{
name: "hide_source",
},
{
name: "swipe_right",
},
{
name: "switch_access_shortcut_add",
},
{
name: "browse_gallery",
},
{
name: "css",
},
{
name: "density_small",
},
{
name: "assistant_direction",
},
{
name: "check_small",
},
{
name: "youtube_searched_for",
},
{
name: "move_up",
},
{
name: "swap_horizontal_circle",
},
{
name: "data_thresholding",
},
{
name: "install_mobile",
},
{
name: "move_down",
},
{
name: "dataset_linked",
},
{
name: "keyboard_command_key",
},
{
name: "view_kanban",
},
{
name: "swipe_down",
},
{
name: "key_off",
},
{
name: "transcribe",
},
{
name: "send_time_extension",
},
{
name: "swipe_down_alt",
},
{
name: "swipe_left_alt",
},
{
name: "swipe_right_alt",
},
{
name: "swipe_up_alt",
},
{
name: "keyboard_option_key",
},
{
name: "cycle",
},
{
name: "rebase",
},
{
name: "rebase_edit",
},
{
name: "empty_dashboard",
},
{
name: "magic_exchange",
},
{
name: "acute",
},
{
name: "point_scan",
},
{
name: "step_into",
},
{
name: "cheer",
},
{
name: "emoticon",
},
{
name: "explosion",
},
{
name: "water_bottle",
},
{
name: "weather_hail",
},
{
name: "syringe",
},
{
name: "pill",
},
{
name: "genetics",
},
{
name: "allergy",
},
{
name: "medical_mask",
},
{
name: "body_fat",
},
{
name: "barefoot",
},
{
name: "infrared",
},
{
name: "wrist",
},
{
name: "metabolism",
},
{
name: "conditions",
},
{
name: "taunt",
},
{
name: "altitude",
},
{
name: "tibia",
},
{
name: "footprint",
},
{
name: "eyeglasses",
},
{
name: "man_3",
},
{
name: "woman_2",
},
{
name: "rheumatology",
},
{
name: "tornado",
},
{
name: "landslide",
},
{
name: "foggy",
},
{
name: "severe_cold",
},
{
name: "tsunami",
},
{
name: "vape_free",
},
{
name: "sign_language",
},
{
name: "emoji_symbols",
},
{
name: "clear_night",
},
{
name: "emoji_food_beverage",
},
{
name: "hive",
},
{
name: "thunderstorm",
},
{
name: "communication",
},
{
name: "rocket",
},
{
name: "pets",
},
{
name: "public",
},
{
name: "quiz",
},
{
name: "mood",
},
{
name: "gavel",
},
{
name: "eco",
},
{
name: "diamond",
},
{
name: "forest",
},
{
name: "rainy",
},
{
name: "skull",
},
];

View File

@ -0,0 +1 @@
export * from "./emoji-icon-picker";

View File

@ -1,4 +1,6 @@
import * as React from "react"; import * as React from "react";
// helpers
import { cn } from "../../helpers";
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
mode?: "primary" | "transparent" | "true-transparent"; mode?: "primary" | "transparent" | "true-transparent";
@ -16,17 +18,20 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
ref={ref} ref={ref}
type={type} type={type}
name={name} name={name}
className={`block rounded-md bg-transparent text-sm placeholder-custom-text-400 focus:outline-none ${ className={cn(
mode === "primary" `block rounded-md bg-transparent text-sm placeholder-custom-text-400 focus:outline-none ${
? "rounded-md border-[0.5px] border-custom-border-200" mode === "primary"
: mode === "transparent" ? "rounded-md border-[0.5px] border-custom-border-200"
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary" : mode === "transparent"
: mode === "true-transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary"
? "rounded border-none bg-transparent ring-0" : mode === "true-transparent"
: "" ? "rounded border-none bg-transparent ring-0"
} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${ : ""
inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : "" } ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${
} ${className}`} inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : ""
}`,
className
)}
{...rest} {...rest}
/> />
); );

View File

@ -2,6 +2,7 @@ export * from "./avatar";
export * from "./breadcrumbs"; export * from "./breadcrumbs";
export * from "./badge"; export * from "./badge";
export * from "./button"; export * from "./button";
export * from "./emoji";
export * from "./dropdowns"; export * from "./dropdowns";
export * from "./form-fields"; export * from "./form-fields";
export * from "./icons"; export * from "./icons";

View File

@ -1 +1,2 @@
export * from "./latest-feature-block"; export * from "./latest-feature-block";
export * from "./project-logo";

View File

@ -0,0 +1,34 @@
// helpers
import { cn } from "helpers/common.helper";
// types
import { TProjectLogoProps } from "@plane/types";
type Props = {
className?: string;
logo: TProjectLogoProps;
};
export const ProjectLogo: React.FC<Props> = (props) => {
const { className, logo } = props;
if (logo.in_use === "icon" && logo.icon)
return (
<span
style={{
color: logo.icon.color,
}}
className={cn("material-symbols-rounded text-base", className)}
>
{logo.icon.name}
</span>
);
if (logo.in_use === "emoji" && logo.emoji)
return (
<span className={cn("text-base", className)}>
{logo.emoji.value?.split("-").map((emoji) => String.fromCodePoint(parseInt(emoji, 10)))}
</span>
);
return <span />;
};

View File

@ -1,15 +1,12 @@
import { useEffect } from "react"; import { useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components // components
// import { NavbarSearch } from "./search";
import { NavbarIssueBoardView } from "./issue-board-view"; import { NavbarIssueBoardView } from "./issue-board-view";
import { NavbarTheme } from "./theme"; import { NavbarTheme } from "./theme";
import { IssueFiltersDropdown } from "components/issues/filters"; import { IssueFiltersDropdown } from "components/issues/filters";
import { ProjectLogo } from "components/common";
// ui // ui
import { Avatar, Button } from "@plane/ui"; import { Avatar, Button } from "@plane/ui";
import { Briefcase } from "lucide-react"; import { Briefcase } from "lucide-react";
@ -19,18 +16,6 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
import { TIssueBoardKeys } from "types/issue"; import { TIssueBoardKeys } from "types/issue";
const renderEmoji = (emoji: string | { name: string; color: string }) => {
if (!emoji) return;
if (typeof emoji === "object")
return (
<span style={{ color: emoji.color }} className="material-symbols-rounded text-lg">
{emoji.name}
</span>
);
else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
};
const IssueNavbar = observer(() => { const IssueNavbar = observer(() => {
const { const {
project: projectStore, project: projectStore,
@ -123,27 +108,15 @@ const IssueNavbar = observer(() => {
<div className="relative flex w-full items-center gap-4 px-5"> <div className="relative flex w-full items-center gap-4 px-5">
{/* project detail */} {/* project detail */}
<div className="flex flex-shrink-0 items-center gap-2"> <div className="flex flex-shrink-0 items-center gap-2">
<div className="flex h-4 w-4 items-center justify-center"> {projectStore.project ? (
{projectStore.project ? ( <span className="h-7 w-7 flex-shrink-0 grid place-items-center">
projectStore.project?.emoji ? ( <ProjectLogo logo={projectStore.project.logo_props} className="text-lg" />
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase"> </span>
{renderEmoji(projectStore.project.emoji)} ) : (
</span> <span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
) : projectStore.project?.icon_prop ? ( <Briefcase className="h-4 w-4" />
<div className="grid h-7 w-7 flex-shrink-0 place-items-center"> </span>
{renderEmoji(projectStore.project.icon_prop)} )}
</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{projectStore.project?.name.charAt(0)}
</span>
)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
<Briefcase className="h-4 w-4" />
</span>
)}
</div>
<div className="line-clamp-1 max-w-[300px] overflow-hidden text-lg font-medium"> <div className="line-clamp-1 max-w-[300px] overflow-hidden text-lg font-medium">
{projectStore?.project?.name || `...`} {projectStore?.project?.name || `...`}
</div> </div>

View File

@ -1,3 +1,5 @@
import { TProjectLogoProps } from "@plane/types";
export interface IWorkspace { export interface IWorkspace {
id: string; id: string;
name: string; name: string;
@ -9,10 +11,8 @@ export interface IProject {
identifier: string; identifier: string;
name: string; name: string;
description: string; description: string;
icon: string;
cover_image: string | null; cover_image: string | null;
icon_prop: string | null; logo_props: TProjectLogoProps;
emoji: string | null;
} }
export interface IProjectSettings { export interface IProjectSettings {

View File

@ -3,9 +3,9 @@ import { observer } from "mobx-react-lite";
// icons // icons
import { Contrast, LayoutGrid, Users } from "lucide-react"; import { Contrast, LayoutGrid, Users } from "lucide-react";
// helpers // helpers
import { renderEmoji } from "helpers/emoji.helper";
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { useProject } from "hooks/store"; import { useProject } from "hooks/store";
import { ProjectLogo } from "components/project";
type Props = { type Props = {
projectIds: string[]; projectIds: string[];
@ -28,15 +28,9 @@ export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((pro
return ( return (
<div key={projectId} className="w-full"> <div key={projectId} className="w-full">
<div className="flex items-center gap-1 text-sm"> <div className="flex items-center gap-1 text-sm">
{project.emoji ? ( <div className="h-6 w-6 grid place-items-center">
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.emoji)}</span> <ProjectLogo logo={project.logo_props} />
) : project.icon_prop ? ( </div>
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.icon_prop)}</div>
) : (
<span className="mr-1 grid h-6 w-6 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{project?.name.charAt(0)}
</span>
)}
<h5 className="flex items-center gap-1"> <h5 className="flex items-center gap-1">
<p className="break-words">{truncateText(project.name, 20)}</p> <p className="break-words">{truncateText(project.name, 20)}</p>
<span className="ml-1 text-xs text-custom-text-200">({project.identifier})</span> <span className="ml-1 text-xs text-custom-text-200">({project.identifier})</span>

View File

@ -3,8 +3,9 @@ import { useRouter } from "next/router";
// hooks // hooks
import { NETWORK_CHOICES } from "constants/project"; import { NETWORK_CHOICES } from "constants/project";
import { renderFormattedDate } from "helpers/date-time.helper"; import { renderFormattedDate } from "helpers/date-time.helper";
import { renderEmoji } from "helpers/emoji.helper";
import { useCycle, useMember, useModule, useProject } from "hooks/store"; import { useCycle, useMember, useModule, useProject } from "hooks/store";
// components
import { ProjectLogo } from "components/project";
// helpers // helpers
// constants // constants
@ -81,15 +82,9 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
) : ( ) : (
<div className="h-full overflow-y-auto"> <div className="h-full overflow-y-auto">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{projectDetails?.emoji ? ( {projectDetails && (
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(projectDetails.emoji)}</div> <span className="h-6 w-6 grid place-items-center flex-shrink-0">
) : projectDetails?.icon_prop ? ( <ProjectLogo logo={projectDetails.logo_props} />
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">
{renderEmoji(projectDetails.icon_prop)}
</div>
) : (
<span className="mr-1 grid h-6 w-6 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{projectDetails?.name.charAt(0)}
</span> </span>
)} )}
<h4 className="break-words font-medium">{projectDetails?.name}</h4> <h4 className="break-words font-medium">{projectDetails?.name}</h4>

View File

@ -7,13 +7,13 @@ import { Avatar, AvatarGroup } from "@plane/ui";
import { WidgetLoader, WidgetProps } from "components/dashboard/widgets"; import { WidgetLoader, WidgetProps } from "components/dashboard/widgets";
import { PROJECT_BACKGROUND_COLORS } from "constants/dashboard"; import { PROJECT_BACKGROUND_COLORS } from "constants/dashboard";
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
import { renderEmoji } from "helpers/emoji.helper";
import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store"; import { useApplication, useEventTracker, useDashboard, useProject, useUser } from "hooks/store";
// components // components
// ui // ui
// helpers // helpers
// types // types
import { TRecentProjectsWidgetResponse } from "@plane/types"; import { TRecentProjectsWidgetResponse } from "@plane/types";
import { ProjectLogo } from "components/project";
// constants // constants
const WIDGET_KEY = "recent_projects"; const WIDGET_KEY = "recent_projects";
@ -38,17 +38,9 @@ const ProjectListItem: React.FC<ProjectListItemProps> = observer((props) => {
<div <div
className={`h-[3.375rem] w-[3.375rem] grid place-items-center rounded border border-transparent flex-shrink-0 ${randomBgColor}`} className={`h-[3.375rem] w-[3.375rem] grid place-items-center rounded border border-transparent flex-shrink-0 ${randomBgColor}`}
> >
{projectDetails.emoji ? ( <div className="h-7 w-7 grid place-items-center">
<span className="grid h-7 w-7 flex-shrink-0 text-2xl place-items-center rounded uppercase"> <ProjectLogo logo={projectDetails.logo_props} className="text-xl" />
{renderEmoji(projectDetails.emoji)} </div>
</span>
) : projectDetails.icon_prop ? (
<div className="grid h-7 w-7 flex-shrink-0 place-items-center">{renderEmoji(projectDetails.icon_prop)}</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{projectDetails.name.charAt(0)}
</span>
)}
</div> </div>
<div className="flex-grow truncate"> <div className="flex-grow truncate">
<h6 className="text-sm text-custom-text-300 font-medium group-hover:underline group-hover:text-custom-text-100 truncate"> <h6 className="text-sm text-custom-text-300 font-medium group-hover:underline group-hover:text-custom-text-100 truncate">

View File

@ -5,12 +5,12 @@ import { Combobox } from "@headlessui/react";
import { Check, ChevronDown, Search } from "lucide-react"; import { Check, ChevronDown, Search } from "lucide-react";
// hooks // hooks
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
import { renderEmoji } from "helpers/emoji.helper";
import { useProject } from "hooks/store"; import { useProject } from "hooks/store";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components // components
import { DropdownButton } from "./buttons"; import { DropdownButton } from "./buttons";
import { ProjectLogo } from "components/project";
// helpers // helpers
// types // types
import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
@ -77,13 +77,11 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
query: `${projectDetails?.name}`, query: `${projectDetails?.name}`,
content: ( content: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="grid place-items-center flex-shrink-0"> {projectDetails && (
{projectDetails?.emoji <span className="grid place-items-center flex-shrink-0 h-4 w-4">
? renderEmoji(projectDetails?.emoji) <ProjectLogo logo={projectDetails?.logo_props} className="text-sm" />
: projectDetails?.icon_prop </span>
? renderEmoji(projectDetails?.icon_prop) )}
: null}
</span>
<span className="flex-grow truncate">{projectDetails?.name}</span> <span className="flex-grow truncate">{projectDetails?.name}</span>
</div> </div>
), ),
@ -169,13 +167,9 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
showTooltip={showTooltip} showTooltip={showTooltip}
variant={buttonVariant} variant={buttonVariant}
> >
{!hideIcon && ( {!hideIcon && selectedProject && (
<span className="grid place-items-center flex-shrink-0"> <span className="grid place-items-center flex-shrink-0 h-4 w-4">
{selectedProject?.emoji <ProjectLogo logo={selectedProject.logo_props} className="text-sm" />
? renderEmoji(selectedProject?.emoji)
: selectedProject?.icon_prop
? renderEmoji(selectedProject?.icon_prop)
: null}
</span> </span>
)} )}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +0,0 @@
export const saveRecentEmoji = (emoji: string) => {
const recentEmojis = localStorage.getItem("recentEmojis");
if (recentEmojis) {
const recentEmojisArray = recentEmojis.split(",");
if (recentEmojisArray.includes(emoji)) {
const index = recentEmojisArray.indexOf(emoji);
recentEmojisArray.splice(index, 1);
}
recentEmojisArray.unshift(emoji);
if (recentEmojisArray.length > 18) {
recentEmojisArray.pop();
}
localStorage.setItem("recentEmojis", recentEmojisArray.join(","));
} else {
localStorage.setItem("recentEmojis", emoji);
}
};
export const getRecentEmojis = () => {
const recentEmojis = localStorage.getItem("recentEmojis");
if (recentEmojis) {
const recentEmojisArray = recentEmojis.split(",");
return recentEmojisArray;
}
return [];
};

View File

@ -1,607 +0,0 @@
{
"material_rounded": [
{
"name": "search"
},
{
"name": "home"
},
{
"name": "menu"
},
{
"name": "close"
},
{
"name": "settings"
},
{
"name": "done"
},
{
"name": "check_circle"
},
{
"name": "favorite"
},
{
"name": "add"
},
{
"name": "delete"
},
{
"name": "arrow_back"
},
{
"name": "star"
},
{
"name": "logout"
},
{
"name": "add_circle"
},
{
"name": "cancel"
},
{
"name": "arrow_drop_down"
},
{
"name": "more_vert"
},
{
"name": "check"
},
{
"name": "check_box"
},
{
"name": "toggle_on"
},
{
"name": "open_in_new"
},
{
"name": "refresh"
},
{
"name": "login"
},
{
"name": "radio_button_unchecked"
},
{
"name": "more_horiz"
},
{
"name": "apps"
},
{
"name": "radio_button_checked"
},
{
"name": "download"
},
{
"name": "remove"
},
{
"name": "toggle_off"
},
{
"name": "bolt"
},
{
"name": "arrow_upward"
},
{
"name": "filter_list"
},
{
"name": "delete_forever"
},
{
"name": "autorenew"
},
{
"name": "key"
},
{
"name": "sort"
},
{
"name": "sync"
},
{
"name": "add_box"
},
{
"name": "block"
},
{
"name": "restart_alt"
},
{
"name": "menu_open"
},
{
"name": "shopping_cart_checkout"
},
{
"name": "expand_circle_down"
},
{
"name": "backspace"
},
{
"name": "undo"
},
{
"name": "done_all"
},
{
"name": "do_not_disturb_on"
},
{
"name": "open_in_full"
},
{
"name": "double_arrow"
},
{
"name": "sync_alt"
},
{
"name": "zoom_in"
},
{
"name": "done_outline"
},
{
"name": "drag_indicator"
},
{
"name": "fullscreen"
},
{
"name": "star_half"
},
{
"name": "settings_accessibility"
},
{
"name": "reply"
},
{
"name": "exit_to_app"
},
{
"name": "unfold_more"
},
{
"name": "library_add"
},
{
"name": "cached"
},
{
"name": "select_check_box"
},
{
"name": "terminal"
},
{
"name": "change_circle"
},
{
"name": "disabled_by_default"
},
{
"name": "swap_horiz"
},
{
"name": "swap_vert"
},
{
"name": "app_registration"
},
{
"name": "download_for_offline"
},
{
"name": "close_fullscreen"
},
{
"name": "file_open"
},
{
"name": "minimize"
},
{
"name": "open_with"
},
{
"name": "dataset"
},
{
"name": "add_task"
},
{
"name": "start"
},
{
"name": "keyboard_voice"
},
{
"name": "create_new_folder"
},
{
"name": "forward"
},
{
"name": "download"
},
{
"name": "settings_applications"
},
{
"name": "compare_arrows"
},
{
"name": "redo"
},
{
"name": "zoom_out"
},
{
"name": "publish"
},
{
"name": "html"
},
{
"name": "token"
},
{
"name": "switch_access_shortcut"
},
{
"name": "fullscreen_exit"
},
{
"name": "sort_by_alpha"
},
{
"name": "delete_sweep"
},
{
"name": "indeterminate_check_box"
},
{
"name": "view_timeline"
},
{
"name": "settings_backup_restore"
},
{
"name": "arrow_drop_down_circle"
},
{
"name": "assistant_navigation"
},
{
"name": "sync_problem"
},
{
"name": "clear_all"
},
{
"name": "density_medium"
},
{
"name": "heart_plus"
},
{
"name": "filter_alt_off"
},
{
"name": "expand"
},
{
"name": "subdirectory_arrow_right"
},
{
"name": "download_done"
},
{
"name": "arrow_outward"
},
{
"name": "123"
},
{
"name": "swipe_left"
},
{
"name": "auto_mode"
},
{
"name": "saved_search"
},
{
"name": "place_item"
},
{
"name": "system_update_alt"
},
{
"name": "javascript"
},
{
"name": "search_off"
},
{
"name": "output"
},
{
"name": "select_all"
},
{
"name": "fit_screen"
},
{
"name": "swipe_up"
},
{
"name": "dynamic_form"
},
{
"name": "hide_source"
},
{
"name": "swipe_right"
},
{
"name": "switch_access_shortcut_add"
},
{
"name": "browse_gallery"
},
{
"name": "css"
},
{
"name": "density_small"
},
{
"name": "assistant_direction"
},
{
"name": "check_small"
},
{
"name": "youtube_searched_for"
},
{
"name": "move_up"
},
{
"name": "swap_horizontal_circle"
},
{
"name": "data_thresholding"
},
{
"name": "install_mobile"
},
{
"name": "move_down"
},
{
"name": "dataset_linked"
},
{
"name": "keyboard_command_key"
},
{
"name": "view_kanban"
},
{
"name": "swipe_down"
},
{
"name": "key_off"
},
{
"name": "transcribe"
},
{
"name": "send_time_extension"
},
{
"name": "swipe_down_alt"
},
{
"name": "swipe_left_alt"
},
{
"name": "swipe_right_alt"
},
{
"name": "swipe_up_alt"
},
{
"name": "keyboard_option_key"
},
{
"name": "cycle"
},
{
"name": "rebase"
},
{
"name": "rebase_edit"
},
{
"name": "empty_dashboard"
},
{
"name": "magic_exchange"
},
{
"name": "acute"
},
{
"name": "point_scan"
},
{
"name": "step_into"
},
{
"name": "cheer"
},
{
"name": "emoticon"
},
{
"name": "explosion"
},
{
"name": "water_bottle"
},
{
"name": "weather_hail"
},
{
"name": "syringe"
},
{
"name": "pill"
},
{
"name": "genetics"
},
{
"name": "allergy"
},
{
"name": "medical_mask"
},
{
"name": "body_fat"
},
{
"name": "barefoot"
},
{
"name": "infrared"
},
{
"name": "wrist"
},
{
"name": "metabolism"
},
{
"name": "conditions"
},
{
"name": "taunt"
},
{
"name": "altitude"
},
{
"name": "tibia"
},
{
"name": "footprint"
},
{
"name": "eyeglasses"
},
{
"name": "man_3"
},
{
"name": "woman_2"
},
{
"name": "rheumatology"
},
{
"name": "tornado"
},
{
"name": "landslide"
},
{
"name": "foggy"
},
{
"name": "severe_cold"
},
{
"name": "tsunami"
},
{
"name": "vape_free"
},
{
"name": "sign_language"
},
{
"name": "emoji_symbols"
},
{
"name": "clear_night"
},
{
"name": "emoji_food_beverage"
},
{
"name": "hive"
},
{
"name": "thunderstorm"
},
{
"name": "communication"
},
{
"name": "rocket"
},
{
"name": "pets"
},
{
"name": "public"
},
{
"name": "quiz"
},
{
"name": "mood"
},
{
"name": "gavel"
},
{
"name": "eco"
},
{
"name": "diamond"
},
{
"name": "forest"
},
{
"name": "rainy"
},
{
"name": "skull"
}
]
}

View File

@ -1,204 +0,0 @@
import React, { useEffect, useState, useRef } from "react";
// headless ui
import { TwitterPicker } from "react-color";
import { Tab, Transition, Popover } from "@headlessui/react";
// react colors
// hooks
import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// types
// emojis
import emojis from "./emojis.json";
import { getRecentEmojis, saveRecentEmoji } from "./helpers";
import icons from "./icons.json";
// helpers
import { Props } from "./types";
const tabOptions = [
{
key: "emoji",
title: "Emoji",
},
{
key: "icon",
title: "Icon",
},
];
const EmojiIconPicker: React.FC<Props> = (props) => {
const { label, value, onChange, onIconColorChange, disabled = false } = props;
// states
const [isOpen, setIsOpen] = useState(false);
const [openColorPicker, setOpenColorPicker] = useState(false);
const [activeColor, setActiveColor] = useState<string>("rgb(var(--color-text-200))");
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
const buttonRef = useRef<HTMLButtonElement>(null);
const emojiPickerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setRecentEmojis(getRecentEmojis());
}, []);
useEffect(() => {
if (!value || value?.length === 0) onChange(getRandomEmoji());
}, [value, onChange]);
useOutsideClickDetector(emojiPickerRef, () => setIsOpen(false));
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), buttonRef, emojiPickerRef);
return (
<Popover className="relative z-[1]">
<Popover.Button
ref={buttonRef}
onClick={() => setIsOpen((prev) => !prev)}
className="outline-none flex items-center justify-center"
disabled={disabled}
>
{label}
</Popover.Button>
<Transition
show={isOpen}
static
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Popover.Panel
ref={emojiPickerRef}
className="fixed z-10 mt-2 w-[250px] rounded-[4px] border border-custom-border-200 bg-custom-background-80 shadow-lg"
>
<div className="h-[230px] w-[250px] overflow-auto rounded-[4px] border border-custom-border-200 bg-custom-background-80 p-2 shadow-xl">
<Tab.Group as="div" className="flex h-full w-full flex-col">
<Tab.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
{tabOptions.map((tab) => (
<Tab key={tab.key} as={React.Fragment}>
{({ selected }) => (
<button
type="button"
onClick={() => {
setOpenColorPicker(false);
}}
className={`-my-1 w-1/2 border-b pb-2 text-center text-sm font-medium outline-none transition-colors ${
selected ? "" : "border-transparent text-custom-text-200"
}`}
>
{tab.title}
</button>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels className="flex-1 overflow-y-auto vertical-scrollbar scrollbar-sm">
<Tab.Panel>
{recentEmojis.length > 0 && (
<div className="py-2">
<h3 className="mb-2 text-xs text-custom-text-200">Recent</h3>
<div className="grid grid-cols-8 gap-2">
{recentEmojis.map((emoji) => (
<button
key={emoji}
type="button"
className="flex h-4 w-4 select-none items-center justify-between text-sm"
onClick={() => {
onChange(emoji);
setIsOpen(false);
}}
>
{renderEmoji(emoji)}
</button>
))}
</div>
</div>
)}
<hr className="mb-2 h-[1px] w-full border-custom-border-200" />
<div>
<div className="grid grid-cols-8 gap-x-2 gap-y-3">
{emojis.map((emoji) => (
<button
type="button"
className="mb-1 flex h-4 w-4 select-none items-center text-sm"
key={emoji}
onClick={() => {
onChange(emoji);
saveRecentEmoji(emoji);
setIsOpen(false);
}}
>
{renderEmoji(emoji)}
</button>
))}
</div>
</div>
</Tab.Panel>
<div className="py-2">
<Tab.Panel className="flex h-full w-full flex-col justify-center">
<div className="relative">
<div className="flex items-center justify-between px-1 pb-2">
{["#FF6B00", "#8CC1FF", "#FCBE1D", "#18904F", "#ADF672", "#05C3FF", "#000000"].map((curCol) => (
<span
key={curCol}
className="h-4 w-4 cursor-pointer rounded-full"
style={{ backgroundColor: curCol }}
onClick={() => setActiveColor(curCol)}
/>
))}
<button
type="button"
onClick={() => setOpenColorPicker((prev) => !prev)}
className="flex items-center gap-1"
>
<span
className="conical-gradient h-4 w-4 rounded-full"
style={{ backgroundColor: activeColor }}
/>
</button>
</div>
<div>
<TwitterPicker
className={`!absolute left-4 top-4 z-10 m-2 ${openColorPicker ? "block" : "hidden"}`}
color={activeColor}
onChange={(color) => {
setActiveColor(color.hex);
if (onIconColorChange) onIconColorChange(color.hex);
}}
triangle="hide"
width="205px"
/>
</div>
</div>
<hr className="mb-1 h-[1px] w-full border-custom-border-200" />
<div className="ml-1 mt-1 grid grid-cols-8 gap-x-2 gap-y-3">
{icons.material_rounded.map((icon, index) => (
<button
key={`${icon.name}-${index}`}
type="button"
className="mb-1 flex h-4 w-4 select-none items-center text-lg"
onClick={() => {
onChange({ name: icon.name, color: activeColor });
setIsOpen(false);
}}
>
<span style={{ color: activeColor }} className="material-symbols-rounded text-lg">
{icon.name}
</span>
</button>
))}
</div>
</Tab.Panel>
</div>
</Tab.Panels>
</Tab.Group>
</div>
</Popover.Panel>
</Transition>
</Popover>
);
};
export default EmojiIconPicker;

View File

@ -1,15 +0,0 @@
export type Props = {
label: React.ReactNode;
value: any;
onChange: (
data:
| string
| {
name: string;
color: string;
}
) => void;
onIconColorChange?: (data: any) => void;
disabled?: boolean;
tabIndex?: number;
};

View File

@ -13,7 +13,6 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
import { renderEmoji } from "helpers/emoji.helper";
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { import {
useApplication, useApplication,
@ -33,6 +32,7 @@ import useLocalStorage from "hooks/use-local-storage";
// helpers // helpers
// types // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
import { ProjectLogo } from "components/project";
// constants // constants
const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => { const CycleDropdownOption: React.FC<{ cycleId: string }> = ({ cycleId }) => {
@ -163,13 +163,9 @@ export const CycleIssuesHeader: React.FC = observer(() => {
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
renderEmoji(currentProjectDetails.emoji) <span className="grid place-items-center flex-shrink-0 h-4 w-4">
) : currentProjectDetails?.icon_prop ? ( <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="flex h-4 w-4 items-center justify-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }
@ -209,7 +205,9 @@ export const CycleIssuesHeader: React.FC = observer(() => {
className="ml-1.5 flex-shrink-0" className="ml-1.5 flex-shrink-0"
placement="bottom-start" placement="bottom-start"
> >
{currentProjectCycleIds?.map((cycleId) => <CycleDropdownOption key={cycleId} cycleId={cycleId} />)} {currentProjectCycleIds?.map((cycleId) => (
<CycleDropdownOption key={cycleId} cycleId={cycleId} />
))}
</CustomMenu> </CustomMenu>
} }
/> />

View File

@ -11,14 +11,15 @@ import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { CYCLE_VIEW_LAYOUTS } from "constants/cycle"; import { CYCLE_VIEW_LAYOUTS } from "constants/cycle";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { renderEmoji } from "helpers/emoji.helper";
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
import { TCycleLayout } from "@plane/types"; import { TCycleLayout } from "@plane/types";
import { ProjectLogo } from "components/project";
export const CyclesHeader: FC = observer(() => { export const CyclesHeader: FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks // store hooks
const { const {
commandPalette: { toggleCreateCycleModal }, commandPalette: { toggleCreateCycleModal },
@ -32,9 +33,6 @@ export const CyclesHeader: FC = observer(() => {
const canUserCreateCycle = const canUserCreateCycle =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
const { workspaceSlug } = router.query as {
workspaceSlug: string;
};
const { setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list"); const { setValue: setCycleLayout } = useLocalStorage<TCycleLayout>("cycle_layout", "list");
const handleCurrentLayout = useCallback( const handleCurrentLayout = useCallback(
@ -58,13 +56,9 @@ export const CyclesHeader: FC = observer(() => {
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
renderEmoji(currentProjectDetails.emoji) <span className="grid place-items-center flex-shrink-0 h-4 w-4">
) : currentProjectDetails?.icon_prop ? ( <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="flex h-4 w-4 items-center justify-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }

View File

@ -13,7 +13,6 @@ import { ModuleMobileHeader } from "components/modules/module-mobile-header";
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
import { renderEmoji } from "helpers/emoji.helper";
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { import {
useApplication, useApplication,
@ -33,6 +32,7 @@ import useLocalStorage from "hooks/use-local-storage";
// helpers // helpers
// types // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
import { ProjectLogo } from "components/project";
// constants // constants
const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => { const ModuleDropdownOption: React.FC<{ moduleId: string }> = ({ moduleId }) => {
@ -64,11 +64,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query as { const { workspaceSlug, projectId, moduleId } = router.query;
workspaceSlug: string;
projectId: string;
moduleId: string;
};
// store hooks // store hooks
const { const {
issuesFilter: { issueFilters, updateFilters }, issuesFilter: { issueFilters, updateFilters },
@ -100,7 +96,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
const handleLayoutChange = useCallback( const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => { (layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId); updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.DISPLAY_FILTERS,
{ layout: layout },
moduleId?.toString()
);
}, },
[workspaceSlug, projectId, moduleId, updateFilters] [workspaceSlug, projectId, moduleId, updateFilters]
); );
@ -119,7 +121,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
else newValues.push(value); else newValues.push(value);
} }
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId); updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{ [key]: newValues },
moduleId?.toString()
);
}, },
[workspaceSlug, projectId, moduleId, issueFilters, updateFilters] [workspaceSlug, projectId, moduleId, issueFilters, updateFilters]
); );
@ -127,7 +135,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
const handleDisplayFilters = useCallback( const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => { (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId); updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.DISPLAY_FILTERS,
updatedDisplayFilter,
moduleId?.toString()
);
}, },
[workspaceSlug, projectId, moduleId, updateFilters] [workspaceSlug, projectId, moduleId, updateFilters]
); );
@ -135,7 +149,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
const handleDisplayProperties = useCallback( const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => { (property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId); updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.DISPLAY_PROPERTIES,
property,
moduleId?.toString()
);
}, },
[workspaceSlug, projectId, moduleId, updateFilters] [workspaceSlug, projectId, moduleId, updateFilters]
); );
@ -166,13 +186,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
renderEmoji(currentProjectDetails.emoji) <span className="grid place-items-center flex-shrink-0 h-4 w-4">
) : currentProjectDetails?.icon_prop ? ( <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }
@ -212,7 +228,9 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
className="ml-1.5 flex-shrink-0" className="ml-1.5 flex-shrink-0"
placement="bottom-start" placement="bottom-start"
> >
{projectModuleIds?.map((moduleId) => <ModuleDropdownOption key={moduleId} moduleId={moduleId} />)} {projectModuleIds?.map((moduleId) => (
<ModuleDropdownOption key={moduleId} moduleId={moduleId} />
))}
</CustomMenu> </CustomMenu>
} }
/> />

View File

@ -10,11 +10,10 @@ import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-ham
// constants // constants
import { MODULE_VIEW_LAYOUTS } from "constants/module"; import { MODULE_VIEW_LAYOUTS } from "constants/module";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
// helper
import { renderEmoji } from "helpers/emoji.helper";
// hooks // hooks
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
import { ProjectLogo } from "components/project";
export const ModulesListHeader: React.FC = observer(() => { export const ModulesListHeader: React.FC = observer(() => {
// router // router
@ -46,13 +45,9 @@ export const ModulesListHeader: React.FC = observer(() => {
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
renderEmoji(currentProjectDetails.emoji) <span className="grid place-items-center flex-shrink-0 h-4 w-4">
) : currentProjectDetails?.icon_prop ? ( <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }

View File

@ -8,9 +8,9 @@ import { Breadcrumbs, Button } from "@plane/ui";
// helpers // helpers
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { renderEmoji } from "helpers/emoji.helper";
// components // components
import { useApplication, usePage, useProject } from "hooks/store"; import { useApplication, usePage, useProject } from "hooks/store";
import { ProjectLogo } from "components/project";
export interface IPagesHeaderProps { export interface IPagesHeaderProps {
showButton?: boolean; showButton?: boolean;
@ -42,13 +42,9 @@ export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
renderEmoji(currentProjectDetails.emoji) <span className="grid place-items-center flex-shrink-0 h-4 w-4">
) : currentProjectDetails?.icon_prop ? ( <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }

View File

@ -8,10 +8,10 @@ import { Breadcrumbs, Button } from "@plane/ui";
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { renderEmoji } from "helpers/emoji.helper";
// constants // constants
// components // components
import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import { useApplication, useEventTracker, useProject, useUser } from "hooks/store";
import { ProjectLogo } from "components/project";
export const PagesHeader = observer(() => { export const PagesHeader = observer(() => {
// router // router
@ -43,13 +43,9 @@ export const PagesHeader = observer(() => {
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
renderEmoji(currentProjectDetails.emoji) <span className="grid place-items-center flex-shrink-0 h-4 w-4">
) : currentProjectDetails?.icon_prop ? ( <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }

View File

@ -7,8 +7,9 @@ import { Breadcrumbs, LayersIcon } from "@plane/ui";
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { ISSUE_DETAILS } from "constants/fetch-keys"; import { ISSUE_DETAILS } from "constants/fetch-keys";
import { renderEmoji } from "helpers/emoji.helper";
import { useProject } from "hooks/store"; import { useProject } from "hooks/store";
// components
import { ProjectLogo } from "components/project";
// ui // ui
// types // types
import { IssueArchiveService } from "services/issue"; import { IssueArchiveService } from "services/issue";
@ -52,13 +53,9 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => {
href={`/${workspaceSlug}/projects`} href={`/${workspaceSlug}/projects`}
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
renderEmoji(currentProjectDetails.emoji) <span className="grid place-items-center flex-shrink-0 h-4 w-4">
) : currentProjectDetails?.icon_prop ? ( <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }

View File

@ -12,10 +12,10 @@ import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-ham
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues"; import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
// helpers // helpers
import { renderEmoji } from "helpers/emoji.helper";
import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store";
// types // types
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
import { ProjectLogo } from "components/project";
export const ProjectArchivedIssuesHeader: FC = observer(() => { export const ProjectArchivedIssuesHeader: FC = observer(() => {
// router // router
@ -91,13 +91,9 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
href={`/${workspaceSlug}/projects`} href={`/${workspaceSlug}/projects`}
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
renderEmoji(currentProjectDetails.emoji) <span className="grid place-items-center flex-shrink-0 h-4 w-4">
) : currentProjectDetails?.icon_prop ? ( <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }

View File

@ -10,9 +10,9 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect
// ui // ui
// helper // helper
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { renderEmoji } from "helpers/emoji.helper";
import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store"; import { useIssues, useLabel, useMember, useProject, useProjectState } from "hooks/store";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
import { ProjectLogo } from "components/project";
export const ProjectDraftIssueHeader: FC = observer(() => { export const ProjectDraftIssueHeader: FC = observer(() => {
// router // router
@ -86,13 +86,9 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
href={`/${workspaceSlug}/projects`} href={`/${workspaceSlug}/projects`}
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
renderEmoji(currentProjectDetails.emoji) <span className="grid place-items-center flex-shrink-0 h-4 w-4">
) : currentProjectDetails?.icon_prop ? ( <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }

View File

@ -10,8 +10,8 @@ import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { CreateInboxIssueModal } from "components/inbox"; import { CreateInboxIssueModal } from "components/inbox";
// helper // helper
import { renderEmoji } from "helpers/emoji.helper";
import { useProject } from "hooks/store"; import { useProject } from "hooks/store";
import { ProjectLogo } from "components/project";
export const ProjectInboxHeader: FC = observer(() => { export const ProjectInboxHeader: FC = observer(() => {
// states // states
@ -35,13 +35,9 @@ export const ProjectInboxHeader: FC = observer(() => {
href={`/${workspaceSlug}/projects`} href={`/${workspaceSlug}/projects`}
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
renderEmoji(currentProjectDetails.emoji) <span className="grid place-items-center flex-shrink-0 h-4 w-4">
) : currentProjectDetails?.icon_prop ? ( <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }

View File

@ -9,12 +9,12 @@ import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { ISSUE_DETAILS } from "constants/fetch-keys"; import { ISSUE_DETAILS } from "constants/fetch-keys";
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
import { renderEmoji } from "helpers/emoji.helper";
import { useApplication, useProject } from "hooks/store"; import { useApplication, useProject } from "hooks/store";
// ui // ui
// helpers // helpers
// services // services
import { IssueService } from "services/issue"; import { IssueService } from "services/issue";
import { ProjectLogo } from "components/project";
// constants // constants
// components // components
@ -51,13 +51,9 @@ export const ProjectIssueDetailsHeader: FC = observer(() => {
href={`/${workspaceSlug}/projects`} href={`/${workspaceSlug}/projects`}
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
renderEmoji(currentProjectDetails.emoji) <span className="grid place-items-center flex-shrink-0 h-4 w-4">
) : currentProjectDetails?.icon_prop ? ( <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }

View File

@ -11,7 +11,6 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect
import { IssuesMobileHeader } from "components/issues/issues-mobile-header"; import { IssuesMobileHeader } from "components/issues/issues-mobile-header";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { renderEmoji } from "helpers/emoji.helper";
import { import {
useApplication, useApplication,
useEventTracker, useEventTracker,
@ -21,11 +20,12 @@ import {
useUser, useUser,
useMember, useMember,
} from "hooks/store"; } from "hooks/store";
import { useIssues } from "hooks/store/use-issues";
// components // components
// ui // ui
// types // types
import { useIssues } from "hooks/store/use-issues";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
import { ProjectLogo } from "components/project";
// constants // constants
// helper // helper
@ -123,17 +123,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
icon={ icon={
currentProjectDetails ? ( currentProjectDetails ? (
currentProjectDetails?.emoji ? ( currentProjectDetails && (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase"> <span className="grid place-items-center flex-shrink-0 h-4 w-4">
{renderEmoji(currentProjectDetails.emoji)} <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
</span>
) : currentProjectDetails?.icon_prop ? (
<div className="grid h-7 w-7 flex-shrink-0 place-items-center">
{renderEmoji(currentProjectDetails.icon_prop)}
</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
) : ( ) : (

View File

@ -7,9 +7,9 @@ import { Breadcrumbs, CustomMenu } from "@plane/ui";
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project"; import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "constants/project";
import { renderEmoji } from "helpers/emoji.helper";
// hooks // hooks
import { useProject, useUser } from "hooks/store"; import { useProject, useUser } from "hooks/store";
import { ProjectLogo } from "components/project";
// constants // constants
// components // components
@ -44,13 +44,9 @@ export const ProjectSettingHeader: FC<IProjectSettingHeader> = observer((props)
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
renderEmoji(currentProjectDetails.emoji) <span className="grid place-items-center flex-shrink-0 h-4 w-4">
) : currentProjectDetails?.icon_prop ? ( <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
renderEmoji(currentProjectDetails.icon_prop)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }

View File

@ -15,7 +15,6 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect
// constants // constants
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { renderEmoji } from "helpers/emoji.helper";
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { import {
useApplication, useApplication,
@ -29,6 +28,7 @@ import {
useUser, useUser,
} from "hooks/store"; } from "hooks/store";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
import { ProjectLogo } from "components/project";
export const ProjectViewIssuesHeader: React.FC = observer(() => { export const ProjectViewIssuesHeader: React.FC = observer(() => {
// router // router
@ -119,17 +119,9 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase"> <span className="grid place-items-center flex-shrink-0 h-4 w-4">
{renderEmoji(currentProjectDetails.emoji)} <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
</span>
) : currentProjectDetails?.icon_prop ? (
<div className="grid h-7 w-7 flex-shrink-0 place-items-center">
{renderEmoji(currentProjectDetails.icon_prop)}
</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }

View File

@ -8,9 +8,9 @@ import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
// helpers // helpers
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { renderEmoji } from "helpers/emoji.helper";
// constants // constants
import { useApplication, useProject, useUser } from "hooks/store"; import { useApplication, useProject, useUser } from "hooks/store";
import { ProjectLogo } from "components/project";
export const ProjectViewsHeader: React.FC = observer(() => { export const ProjectViewsHeader: React.FC = observer(() => {
// router // router
@ -42,17 +42,9 @@ export const ProjectViewsHeader: React.FC = observer(() => {
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`} href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"} label={currentProjectDetails?.name ?? "Project"}
icon={ icon={
currentProjectDetails?.emoji ? ( currentProjectDetails && (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase"> <span className="grid place-items-center flex-shrink-0 h-4 w-4">
{renderEmoji(currentProjectDetails.emoji)} <ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
</span>
) : currentProjectDetails?.icon_prop ? (
<div className="grid h-7 w-7 flex-shrink-0 place-items-center">
{renderEmoji(currentProjectDetails.icon_prop)}
</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{currentProjectDetails?.name.charAt(0)}
</span> </span>
) )
} }

View File

@ -1,9 +1,9 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { X } from "lucide-react"; import { X } from "lucide-react";
// hooks // hooks
import { renderEmoji } from "helpers/emoji.helper";
import { useProject } from "hooks/store"; import { useProject } from "hooks/store";
// helpers // components
import { ProjectLogo } from "components/project";
type Props = { type Props = {
handleRemove: (val: string) => void; handleRemove: (val: string) => void;
@ -25,15 +25,9 @@ export const AppliedProjectFilters: React.FC<Props> = observer((props) => {
return ( return (
<div key={projectId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs"> <div key={projectId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
{projectDetails.emoji ? ( <span className="grid place-items-center flex-shrink-0 h-4 w-4">
<span className="grid flex-shrink-0 place-items-center">{renderEmoji(projectDetails.emoji)}</span> <ProjectLogo logo={projectDetails.logo_props} className="text-sm" />
) : projectDetails.icon_prop ? ( </span>
<div className="-my-1 grid flex-shrink-0 place-items-center">{renderEmoji(projectDetails.icon_prop)}</div>
) : (
<span className="mr-1 grid flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{projectDetails?.name.charAt(0)}
</span>
)}
<span className="normal-case">{projectDetails.name}</span> <span className="normal-case">{projectDetails.name}</span>
{editable && ( {editable && (
<button <button

View File

@ -4,8 +4,9 @@ import { observer } from "mobx-react";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
import { FilterHeader, FilterOption } from "components/issues"; import { FilterHeader, FilterOption } from "components/issues";
// hooks // hooks
import { renderEmoji } from "helpers/emoji.helper";
import { useProject } from "hooks/store"; import { useProject } from "hooks/store";
// components
import { ProjectLogo } from "components/project";
// ui // ui
// helpers // helpers
@ -52,19 +53,9 @@ export const FilterProjects: React.FC<Props> = observer((props) => {
isChecked={appliedFilters?.includes(project.id) ? true : false} isChecked={appliedFilters?.includes(project.id) ? true : false}
onClick={() => handleUpdate(project.id)} onClick={() => handleUpdate(project.id)}
icon={ icon={
project.emoji ? ( <span className="grid place-items-center flex-shrink-0 h-4 w-4">
<span className="grid flex-shrink-0 place-items-center text-sm"> <ProjectLogo logo={project.logo_props} className="text-sm" />
{renderEmoji(project.emoji)} </span>
</span>
) : project.icon_prop ? (
<div className="-my-1 grid flex-shrink-0 place-items-center">
{renderEmoji(project.icon_prop)}
</div>
) : (
<span className="mr-1 grid flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{project?.name.charAt(0)}
</span>
)
} }
title={project.name} title={project.name}
/> />

View File

@ -1,9 +1,10 @@
import { ContrastIcon } from "lucide-react"; import { ContrastIcon } from "lucide-react";
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui"; import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
// components
import { ProjectLogo } from "components/project";
// stores // stores
import { ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_PRIORITIES } from "constants/issue";
import { STATE_GROUPS } from "constants/state"; import { STATE_GROUPS } from "constants/state";
import { renderEmoji } from "helpers/emoji.helper";
import { ICycleStore } from "store/cycle.store"; import { ICycleStore } from "store/cycle.store";
import { ILabelStore } from "store/label.store"; import { ILabelStore } from "store/label.store";
import { IMemberRootStore } from "store/member"; import { IMemberRootStore } from "store/member";
@ -62,7 +63,11 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined
return { return {
id: project.id, id: project.id,
name: project.name, name: project.name,
icon: <div className="h-6 w-6">{renderEmoji(project.emoji || "")}</div>, icon: (
<div className="w-6 h-6 grid place-items-center flex-shrink-0">
<ProjectLogo logo={project.logo_props} />
</div>
),
payload: { project_id: project.id }, payload: { project_id: project.id },
}; };
}) as any; }) as any;

View File

@ -13,26 +13,28 @@ import { Loader, Tooltip } from "@plane/ui";
import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys"; import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
// helpers // helpers
import { renderFormattedDate } from "helpers/date-time.helper"; import { renderFormattedDate } from "helpers/date-time.helper";
import { renderEmoji } from "helpers/emoji.helper";
// hooks // hooks
import { useApplication, useUser } from "hooks/store"; import { useApplication, useProject, useUser } from "hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// services // services
import { UserService } from "services/user.service"; import { UserService } from "services/user.service";
// components // components
import { ProfileSidebarTime } from "./time"; import { ProfileSidebarTime } from "./time";
import { ProjectLogo } from "components/project";
// services // services
const userService = new UserService(); const userService = new UserService();
export const ProfileSidebar = observer(() => { export const ProfileSidebar = observer(() => {
// refs
const ref = useRef<HTMLDivElement>(null);
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, userId } = router.query; const { workspaceSlug, userId } = router.query;
// store hooks // store hooks
const { currentUser } = useUser(); const { currentUser } = useUser();
const { theme: themeStore } = useApplication(); const { theme: themeStore } = useApplication();
const ref = useRef<HTMLDivElement>(null); const { getProjectById } = useProject();
const { data: userProjectsData } = useSWR( const { data: userProjectsData } = useSWR(
workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null, workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null,
@ -130,6 +132,8 @@ export const ProfileSidebar = observer(() => {
</div> </div>
<div className="mt-9 divide-y divide-custom-border-100"> <div className="mt-9 divide-y divide-custom-border-100">
{userProjectsData.project_data.map((project, index) => { {userProjectsData.project_data.map((project, index) => {
const projectDetails = getProjectById(project.id);
const totalIssues = const totalIssues =
project.created_issues + project.assigned_issues + project.pending_issues + project.completed_issues; project.created_issues + project.assigned_issues + project.pending_issues + project.completed_issues;
@ -138,26 +142,18 @@ export const ProfileSidebar = observer(() => {
? 0 ? 0
: Math.round((project.completed_issues / project.assigned_issues) * 100); : Math.round((project.completed_issues / project.assigned_issues) * 100);
if (!projectDetails) return null;
return ( return (
<Disclosure key={project.id} as="div" className={`${index === 0 ? "pb-3" : "py-3"}`}> <Disclosure key={project.id} as="div" className={`${index === 0 ? "pb-3" : "py-3"}`}>
{({ open }) => ( {({ open }) => (
<div className="w-full"> <div className="w-full">
<Disclosure.Button className="flex w-full items-center justify-between gap-2"> <Disclosure.Button className="flex w-full items-center justify-between gap-2">
<div className="flex w-3/4 items-center gap-2"> <div className="flex w-3/4 items-center gap-2">
{project.emoji ? ( <span className="grid place-items-center flex-shrink-0 h-7 w-7">
<div className="grid h-7 w-7 flex-shrink-0 place-items-center"> <ProjectLogo logo={projectDetails.logo_props} />
{renderEmoji(project.emoji)} </span>
</div> <div className="truncate break-words text-sm font-medium">{projectDetails.name}</div>
) : project.icon_prop ? (
<div className="grid h-7 w-7 flex-shrink-0 place-items-center">
{renderEmoji(project.icon_prop)}
</div>
) : (
<div className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-custom-background-90 text-xs uppercase text-custom-text-100">
{project?.name.charAt(0)}
</div>
)}
<div className="truncate break-words text-sm font-medium">{project.name}</div>
</div> </div>
<div className="flex flex-shrink-0 items-center gap-2"> <div className="flex flex-shrink-0 items-center gap-2">
{project.assigned_issues > 0 && ( {project.assigned_issues > 0 && (

View File

@ -6,14 +6,14 @@ import { LinkIcon, Lock, Pencil, Star } from "lucide-react";
// ui // ui
import { Avatar, AvatarGroup, Button, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; import { Avatar, AvatarGroup, Button, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui";
// components // components
import { DeleteProjectModal, JoinProjectModal, EUserProjectRoles } from "components/project"; import { DeleteProjectModal, JoinProjectModal, ProjectLogo } from "components/project";
// helpers // helpers
import { renderEmoji } from "helpers/emoji.helper";
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
// hooks // hooks
import { useProject } from "hooks/store"; import { useProject } from "hooks/store";
// types // types
import type { IProject } from "@plane/types"; import type { IProject } from "@plane/types";
import { EUserProjectRoles } from "constants/project";
// constants // constants
export type ProjectCardProps = { export type ProjectCardProps = {
@ -123,13 +123,9 @@ export const ProjectCard: React.FC<ProjectCardProps> = observer((props) => {
<div className="absolute bottom-4 z-10 flex h-10 w-full items-center justify-between gap-3 px-4"> <div className="absolute bottom-4 z-10 flex h-10 w-full items-center justify-between gap-3 px-4">
<div className="flex flex-grow items-center gap-2.5 truncate"> <div className="flex flex-grow items-center gap-2.5 truncate">
<div className="item-center flex h-9 w-9 flex-shrink-0 justify-center rounded bg-white/90"> <div className="flex item-center justify-center h-9 w-9 flex-shrink-0 rounded bg-white/90">
<span className="flex items-center justify-center"> <span className="grid place-items-center">
{project.emoji <ProjectLogo logo={project.logo_props} />
? renderEmoji(project.emoji)
: project.icon_prop
? renderEmoji(project.icon_prop)
: null}
</span> </span>
</div> </div>

View File

@ -4,19 +4,30 @@ import { useForm, Controller } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { X } from "lucide-react"; import { X } from "lucide-react";
// ui // ui
import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; import {
Button,
CustomEmojiIconPicker,
CustomSelect,
EmojiIconPickerTypes,
Input,
setToast,
TextArea,
TOAST_TYPE,
} from "@plane/ui";
// components // components
import { ImagePickerPopover } from "components/core"; import { ImagePickerPopover } from "components/core";
import { MemberDropdown } from "components/dropdowns"; import { MemberDropdown } from "components/dropdowns";
import EmojiIconPicker from "components/emoji-icon-picker";
// constants // constants
import { PROJECT_CREATED } from "constants/event-tracker"; import { PROJECT_CREATED } from "constants/event-tracker";
import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project";
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
// helpers // helpers
import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; import { convertHexEmojiToDecimal, getRandomEmoji } from "helpers/emoji.helper";
// hooks // hooks
import { useEventTracker, useProject, useUser } from "hooks/store"; import { useEventTracker, useProject, useUser } from "hooks/store";
import { projectIdentifierSanitizer } from "helpers/project.helper";
import { ProjectLogo } from "./project-logo";
import { IProject } from "@plane/types";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -29,6 +40,21 @@ interface IIsGuestCondition {
onClose: () => void; onClose: () => void;
} }
const defaultValues: Partial<IProject> = {
cover_image: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)],
description: "",
logo_props: {
in_use: "emoji",
emoji: {
value: getRandomEmoji(),
},
},
identifier: "",
name: "",
network: 2,
project_lead: null,
};
const IsGuestCondition: FC<IIsGuestCondition> = ({ onClose }) => { const IsGuestCondition: FC<IIsGuestCondition> = ({ onClose }) => {
useEffect(() => { useEffect(() => {
onClose(); onClose();
@ -42,19 +68,6 @@ const IsGuestCondition: FC<IIsGuestCondition> = ({ onClose }) => {
return null; return null;
}; };
export interface ICreateProjectForm {
name: string;
identifier: string;
description: string;
emoji_and_icon: string;
network: number;
project_lead_member: string;
project_lead: string;
cover_image: string;
icon_prop: any;
emoji: string;
}
export const CreateProjectModal: FC<Props> = observer((props) => { export const CreateProjectModal: FC<Props> = observer((props) => {
const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props; const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props;
// store // store
@ -66,7 +79,6 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
// states // states
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
// form info // form info
const cover_image = PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)];
const { const {
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
handleSubmit, handleSubmit,
@ -74,28 +86,20 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
control, control,
watch, watch,
setValue, setValue,
} = useForm<ICreateProjectForm>({ } = useForm<IProject>({
defaultValues: { defaultValues,
cover_image,
description: "",
emoji_and_icon: getRandomEmoji(),
identifier: "",
name: "",
network: 2,
project_lead: undefined,
},
reValidateMode: "onChange", reValidateMode: "onChange",
}); });
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
if (currentWorkspaceRole && isOpen) if (currentWorkspaceRole && isOpen)
if (currentWorkspaceRole < EUserWorkspaceRoles.MEMBER) return <IsGuestCondition onClose={onClose} />; if (currentWorkspaceRole < EUserWorkspaceRoles.MEMBER) return <IsGuestCondition onClose={onClose} />;
const handleClose = () => { const handleClose = () => {
onClose(); onClose();
setIsChangeInIdentifierRequired(true); setIsChangeInIdentifierRequired(true);
reset(); setTimeout(() => {
reset();
}, 300);
}; };
const handleAddToFavorites = (projectId: string) => { const handleAddToFavorites = (projectId: string) => {
@ -110,18 +114,11 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
}); });
}; };
const onSubmit = async (formData: ICreateProjectForm) => { const onSubmit = async (formData: Partial<IProject>) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { emoji_and_icon, project_lead_member, ...payload } = formData;
if (typeof formData.emoji_and_icon === "object") payload.icon_prop = formData.emoji_and_icon;
else payload.emoji = formData.emoji_and_icon;
payload.project_lead = formData.project_lead_member;
// Upper case identifier // Upper case identifier
payload.identifier = payload.identifier.toUpperCase(); formData.identifier = formData.identifier?.toUpperCase();
return createProject(workspaceSlug.toString(), payload) return createProject(workspaceSlug.toString(), formData)
.then((res) => { .then((res) => {
const newPayload = { const newPayload = {
...res, ...res,
@ -151,7 +148,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
captureProjectEvent({ captureProjectEvent({
eventName: PROJECT_CREATED, eventName: PROJECT_CREATED,
payload: { payload: {
...payload, ...formData,
state: "FAILED", state: "FAILED",
}, },
}); });
@ -165,13 +162,13 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
return; return;
} }
if (e.target.value === "") setValue("identifier", ""); if (e.target.value === "") setValue("identifier", "");
else setValue("identifier", e.target.value.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, "").substring(0, 5)); else setValue("identifier", projectIdentifierSanitizer(e.target.value).substring(0, 5));
onChange(e); onChange(e);
}; };
const handleIdentifierChange = (onChange: any) => (e: ChangeEvent<HTMLInputElement>) => { const handleIdentifierChange = (onChange: any) => (e: ChangeEvent<HTMLInputElement>) => {
const { value } = e.target; const { value } = e.target;
const alphanumericValue = value.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, ""); const alphanumericValue = projectIdentifierSanitizer(value);
setIsChangeInIdentifierRequired(false); setIsChangeInIdentifierRequired(false);
onChange(alphanumericValue); onChange(alphanumericValue);
}; };
@ -204,11 +201,11 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
> >
<Dialog.Panel className="w-full transform rounded-lg bg-custom-background-100 p-3 text-left shadow-custom-shadow-md transition-all sm:w-3/5 lg:w-1/2 xl:w-2/5"> <Dialog.Panel className="w-full transform rounded-lg bg-custom-background-100 p-3 text-left shadow-custom-shadow-md transition-all sm:w-3/5 lg:w-1/2 xl:w-2/5">
<div className="group relative h-44 w-full rounded-lg bg-custom-background-80"> <div className="group relative h-44 w-full rounded-lg bg-custom-background-80">
{watch("cover_image") !== null && ( {watch("cover_image") && (
<img <img
src={watch("cover_image")!} src={watch("cover_image")!}
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover" className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
alt="Cover Image" alt="Cover image"
/> />
)} )}
@ -218,30 +215,50 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
</button> </button>
</div> </div>
<div className="absolute bottom-2 right-2"> <div className="absolute bottom-2 right-2">
<ImagePickerPopover <Controller
label="Change Cover" name="cover_image"
onChange={(image) => {
setValue("cover_image", image);
}}
control={control} control={control}
value={watch("cover_image")} render={({ field: { value, onChange } }) => (
tabIndex={9} <ImagePickerPopover
label="Change Cover"
onChange={onChange}
control={control}
value={value}
tabIndex={9}
/>
)}
/> />
</div> </div>
<div className="absolute -bottom-[22px] left-3"> <div className="absolute -bottom-[22px] left-3">
<Controller <Controller
name="emoji_and_icon" name="logo_props"
control={control} control={control}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<EmojiIconPicker <CustomEmojiIconPicker
label={ label={
<div className="grid h-[44px] w-[44px] place-items-center rounded-md bg-custom-background-80 text-lg outline-none"> <span className="grid h-11 w-11 place-items-center rounded-md bg-custom-background-80">
{value ? renderEmoji(value) : "Icon"} <ProjectLogo logo={value} className="text-xl" />
</div> </span>
}
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,
});
}}
defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined}
defaultOpen={
value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON
} }
onChange={onChange}
value={value}
tabIndex={10}
/> />
)} )}
/> />
@ -275,7 +292,9 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
/> />
)} )}
/> />
<span className="text-xs text-red-500">{errors?.name?.message}</span> <span className="text-xs text-red-500">
<>{errors?.name?.message}</>
</span>
</div> </div>
<div> <div>
<Controller <Controller
@ -310,7 +329,9 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
/> />
)} )}
/> />
<span className="text-xs text-red-500">{errors?.identifier?.message}</span> <span className="text-xs text-red-500">
<>{errors?.identifier?.message}</>
</span>
</div> </div>
<div className="md:col-span-4"> <div className="md:col-span-4">
<Controller <Controller
@ -336,57 +357,65 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
<Controller <Controller
name="network" name="network"
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => {
<div className="flex-shrink-0" tabIndex={4}> const currentNetwork = NETWORK_CHOICES.find((n) => n.key === value);
<CustomSelect
value={value} return (
onChange={onChange} <div className="flex-shrink-0" tabIndex={4}>
label={ <CustomSelect
<div className="flex items-center gap-1"> value={value}
{currentNetwork ? ( onChange={onChange}
<> label={
<currentNetwork.icon className="h-3 w-3" /> <div className="flex items-center gap-1">
{currentNetwork.label} {currentNetwork ? (
</> <>
) : ( <currentNetwork.icon className="h-3 w-3" />
<span className="text-custom-text-400">Select Network</span> {currentNetwork.label}
)} </>
</div> ) : (
} <span className="text-custom-text-400">Select network</span>
placement="bottom-start" )}
noChevron
tabIndex={4}
>
{NETWORK_CHOICES.map((network) => (
<CustomSelect.Option key={network.key} value={network.key}>
<div className="flex items-start gap-2">
<network.icon className="h-3.5 w-3.5" />
<div className="-mt-1">
<p>{network.label}</p>
<p className="text-xs text-custom-text-400">{network.description}</p>
</div>
</div> </div>
</CustomSelect.Option> }
))} placement="bottom-start"
</CustomSelect> noChevron
</div> tabIndex={4}
)} >
{NETWORK_CHOICES.map((network) => (
<CustomSelect.Option key={network.key} value={network.key}>
<div className="flex items-start gap-2">
<network.icon className="h-3.5 w-3.5" />
<div className="-mt-1">
<p>{network.label}</p>
<p className="text-xs text-custom-text-400">{network.description}</p>
</div>
</div>
</CustomSelect.Option>
))}
</CustomSelect>
</div>
);
}}
/> />
<Controller <Controller
name="project_lead_member" name="project_lead"
control={control} control={control}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => {
<div className="h-7 flex-shrink-0" tabIndex={5}> if (value === undefined || value === null || typeof value === "string")
<MemberDropdown return (
value={value} <div className="h-7 flex-shrink-0" tabIndex={5}>
onChange={onChange} <MemberDropdown
placeholder="Lead" value={value}
multiple={false} onChange={onChange}
buttonVariant="border-with-text" placeholder="Lead"
tabIndex={5} multiple={false}
/> buttonVariant="border-with-text"
</div> tabIndex={5}
)} />
</div>
);
else return <></>;
}}
/> />
</div> </div>
</div> </div>
@ -396,7 +425,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
Cancel Cancel
</Button> </Button>
<Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={7}> <Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={7}>
{isSubmitting ? "Creating..." : "Create Project"} {isSubmitting ? "Creating" : "Create project"}
</Button> </Button>
</div> </div>
</form> </form>

View File

@ -3,22 +3,31 @@ import { Controller, useForm } from "react-hook-form";
// icons // icons
import { Lock } from "lucide-react"; import { Lock } from "lucide-react";
// ui // ui
import { Button, CustomSelect, Input, TextArea, TOAST_TYPE, setToast } from "@plane/ui"; import {
Button,
CustomSelect,
Input,
TextArea,
TOAST_TYPE,
setToast,
CustomEmojiIconPicker,
EmojiIconPickerTypes,
} from "@plane/ui";
// components // components
import { ImagePickerPopover } from "components/core"; import { ImagePickerPopover } from "components/core";
import EmojiIconPicker from "components/emoji-icon-picker";
// constants // constants
import { PROJECT_UPDATED } from "constants/event-tracker"; import { PROJECT_UPDATED } from "constants/event-tracker";
import { NETWORK_CHOICES } from "constants/project"; import { NETWORK_CHOICES } from "constants/project";
// helpers // helpers
import { renderFormattedDate } from "helpers/date-time.helper"; import { renderFormattedDate } from "helpers/date-time.helper";
import { renderEmoji } from "helpers/emoji.helper";
// hooks // hooks
import { useEventTracker, useProject } from "hooks/store"; import { useEventTracker, useProject } from "hooks/store";
// services // services
import { ProjectService } from "services/project"; import { ProjectService } from "services/project";
// types // types
import { IProject, IWorkspace } from "@plane/types"; import { IProject, IWorkspace } from "@plane/types";
import { ProjectLogo } from "./project-logo";
import { convertHexEmojiToDecimal } from "helpers/emoji.helper";
export interface IProjectDetailsForm { export interface IProjectDetailsForm {
project: IProject; project: IProject;
workspaceSlug: string; workspaceSlug: string;
@ -46,7 +55,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
} = useForm<IProject>({ } = useForm<IProject>({
defaultValues: { defaultValues: {
...project, ...project,
emoji_and_icon: project.emoji ?? project.icon_prop,
workspace: (project.workspace as IWorkspace).id, workspace: (project.workspace as IWorkspace).id,
}, },
}); });
@ -55,7 +63,6 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
if (project && projectId !== getValues("id")) { if (project && projectId !== getValues("id")) {
reset({ reset({
...project, ...project,
emoji_and_icon: project.emoji ?? project.icon_prop,
workspace: (project.workspace as IWorkspace).id, workspace: (project.workspace as IWorkspace).id,
}); });
} }
@ -109,14 +116,9 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
identifier: formData.identifier, identifier: formData.identifier,
description: formData.description, description: formData.description,
cover_image: formData.cover_image, cover_image: formData.cover_image,
logo_props: formData.logo_props,
}; };
if (typeof formData.emoji_and_icon === "object") {
payload.emoji = null;
payload.icon_prop = formData.emoji_and_icon;
} else {
payload.emoji = formData.emoji_and_icon;
payload.icon_prop = null;
}
if (project.identifier !== formData.identifier) if (project.identifier !== formData.identifier)
await projectService await projectService
.checkProjectIdentifierAvailability(workspaceSlug as string, payload.identifier ?? "") .checkProjectIdentifierAvailability(workspaceSlug as string, payload.identifier ?? "")
@ -139,20 +141,37 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
<div className="z-5 absolute bottom-4 flex w-full items-end justify-between gap-3 px-4"> <div className="z-5 absolute bottom-4 flex w-full items-end justify-between gap-3 px-4">
<div className="flex flex-grow gap-3 truncate"> <div className="flex flex-grow gap-3 truncate">
<div className="flex h-[52px] w-[52px] flex-shrink-0 items-center justify-center rounded-lg bg-custom-background-90"> <div className="flex h-[52px] w-[52px] flex-shrink-0 items-center justify-center rounded-lg bg-custom-background-90">
<div className="grid h-7 w-7 place-items-center"> <Controller
<Controller control={control}
control={control} name="logo_props"
name="emoji_and_icon" render={({ field: { value, onChange } }) => (
render={({ field: { value, onChange } }) => ( <CustomEmojiIconPicker
<EmojiIconPicker label={
label={value ? renderEmoji(value) : "Icon"} <span className="grid h-7 w-7 place-items-center">
value={value} <ProjectLogo logo={value} className="text-lg" />
onChange={onChange} </span>
disabled={!isAdmin} }
/> onChange={(val) => {
)} let logoValue = {};
/>
</div> 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,
});
}}
defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined}
defaultOpen={value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON}
disabled={!isAdmin}
/>
)}
/>
</div> </div>
<div className="flex flex-col gap-1 truncate text-white"> <div className="flex flex-col gap-1 truncate text-white">
<span className="truncate text-lg font-semibold">{watch("name")}</span> <span className="truncate text-lg font-semibold">{watch("name")}</span>

View File

@ -14,6 +14,7 @@ export * from "./sidebar-list";
export * from "./integration-card"; export * from "./integration-card";
export * from "./member-list"; export * from "./member-list";
export * from "./member-list-item"; export * from "./member-list-item";
export * from "./project-logo";
export * from "./project-settings-member-defaults"; export * from "./project-settings-member-defaults";
export * from "./send-project-invitation-modal"; export * from "./send-project-invitation-modal";
export * from "./confirm-project-member-remove"; export * from "./confirm-project-member-remove";

View File

@ -0,0 +1,34 @@
// helpers
import { cn } from "helpers/common.helper";
// types
import { TProjectLogoProps } from "@plane/types";
type Props = {
className?: string;
logo: TProjectLogoProps;
};
export const ProjectLogo: React.FC<Props> = (props) => {
const { className, logo } = props;
if (logo.in_use === "icon" && logo.icon)
return (
<span
style={{
color: logo.icon.color,
}}
className={cn("material-symbols-rounded text-base", className)}
>
{logo.icon.name}
</span>
);
if (logo.in_use === "emoji" && logo.emoji)
return (
<span className={cn("text-base", className)}>
{logo.emoji.value?.split("-").map((emoji) => String.fromCodePoint(parseInt(emoji, 10)))}
</span>
);
return <span />;
};

View File

@ -29,10 +29,9 @@ import {
LayersIcon, LayersIcon,
setPromiseToast, setPromiseToast,
} from "@plane/ui"; } from "@plane/ui";
import { LeaveProjectModal, PublishProjectModal } from "components/project"; import { LeaveProjectModal, ProjectLogo, PublishProjectModal } from "components/project";
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { cn } from "helpers/common.helper"; import { cn } from "helpers/common.helper";
import { renderEmoji } from "helpers/emoji.helper";
import { getNumberCount } from "helpers/string.helper"; import { getNumberCount } from "helpers/string.helper";
// hooks // hooks
import { useApplication, useEventTracker, useInbox, useProject } from "hooks/store"; import { useApplication, useEventTracker, useInbox, useProject } from "hooks/store";
@ -100,23 +99,21 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
const [publishModalOpen, setPublishModal] = useState(false); const [publishModalOpen, setPublishModal] = useState(false);
const [isMenuActive, setIsMenuActive] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false);
// refs
const actionSectionRef = useRef<HTMLDivElement | null>(null);
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId: URLProjectId } = router.query; const { workspaceSlug, projectId: URLProjectId } = router.query;
// derived values // derived values
const project = getProjectById(projectId); const project = getProjectById(projectId);
const isCollapsed = themeStore.sidebarCollapsed;
const inboxesMap = project?.inbox_view ? getInboxesByProjectId(projectId) : undefined;
const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined;
// auth
const isAdmin = project?.member_role === EUserProjectRoles.ADMIN; const isAdmin = project?.member_role === EUserProjectRoles.ADMIN;
const isViewerOrGuest = const isViewerOrGuest =
project?.member_role && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(project.member_role); project?.member_role && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(project.member_role);
const isCollapsed = themeStore.sidebarCollapsed;
const actionSectionRef = useRef<HTMLDivElement | null>(null);
const inboxesMap = project?.inbox_view ? getInboxesByProjectId(projectId) : undefined;
const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined;
const handleAddToFavorites = () => { const handleAddToFavorites = () => {
if (!workspaceSlug || !project) return; if (!workspaceSlug || !project) return;
@ -178,9 +175,13 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
{({ open }) => ( {({ open }) => (
<> <>
<div <div
className={`group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-10 hover:bg-custom-sidebar-background-80 ${ className={cn(
snapshot?.isDragging ? "opacity-60" : "" "group relative flex w-full items-center rounded-md px-2 py-1 text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80",
} ${isMenuActive ? "!bg-custom-sidebar-background-80" : ""}`} {
"opacity-60": snapshot?.isDragging,
"bg-custom-sidebar-background-80": isMenuActive,
}
)}
> >
{provided && !disableDrag && ( {provided && !disableDrag && (
<Tooltip <Tooltip
@ -189,11 +190,14 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
> >
<button <button
type="button" type="button"
className={`absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400 ${ className={cn(
isCollapsed ? "" : "group-hover:!flex" "absolute -left-2.5 top-1/2 hidden -translate-y-1/2 rounded p-0.5 text-custom-sidebar-text-400",
} ${project.sort_order === null ? "cursor-not-allowed opacity-60" : ""} ${ {
isMenuActive ? "!flex" : "" "group-hover:flex": !isCollapsed,
}`} "cursor-not-allowed opacity-60": project.sort_order === null,
flex: isMenuActive,
}
)}
{...provided?.dragHandleProps} {...provided?.dragHandleProps}
> >
<MoreVertical className="h-3.5" /> <MoreVertical className="h-3.5" />
@ -204,36 +208,32 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
<Tooltip tooltipContent={`${project.name}`} position="right" className="ml-2" disabled={!isCollapsed}> <Tooltip tooltipContent={`${project.name}`} position="right" className="ml-2" disabled={!isCollapsed}>
<Disclosure.Button <Disclosure.Button
as="div" as="div"
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${ className={cn(
isCollapsed ? "justify-center" : `justify-between` "flex items-center justify-between flex-grow cursor-pointer select-none truncate text-left text-sm font-medium",
}`} {
"justify-center": isCollapsed,
}
)}
> >
<div <div
className={`flex w-full flex-grow items-center gap-x-2 truncate ${ className={cn("w-full flex-grow flex items-center gap-1 truncate", {
isCollapsed ? "justify-center" : "" "justify-center": isCollapsed,
}`} })}
> >
{project.emoji ? ( <div className="h-7 w-7 grid place-items-center">
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase"> <ProjectLogo logo={project.logo_props} />
{renderEmoji(project.emoji)} </div>
</span>
) : project.icon_prop ? (
<div className="grid h-7 w-7 flex-shrink-0 place-items-center">
{renderEmoji(project.icon_prop)}
</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{project?.name.charAt(0)}
</span>
)}
{!isCollapsed && <p className="truncate text-custom-sidebar-text-200">{project.name}</p>} {!isCollapsed && <p className="truncate text-custom-sidebar-text-200">{project.name}</p>}
</div> </div>
{!isCollapsed && ( {!isCollapsed && (
<ChevronDown <ChevronDown
className={`hidden h-4 w-4 flex-shrink-0 ${open ? "rotate-180" : ""} ${ className={cn(
isMenuActive ? "!block" : "" "hidden h-4 w-4 flex-shrink-0 mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:block",
} mb-0.5 text-custom-sidebar-text-400 duration-300 group-hover:!block`} {
"rotate-180": open,
block: isMenuActive,
}
)}
/> />
)} )}
</Disclosure.Button> </Disclosure.Button>
@ -250,7 +250,9 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
<MoreHorizontal className="h-3.5 w-3.5" /> <MoreHorizontal className="h-3.5 w-3.5" />
</div> </div>
} }
className={`hidden flex-shrink-0 group-hover:block ${isMenuActive ? "!block" : ""}`} className={cn("hidden flex-shrink-0 group-hover:block", {
"!block": isMenuActive,
})}
buttonClassName="!text-custom-sidebar-text-400" buttonClassName="!text-custom-sidebar-text-400"
ellipsis ellipsis
placement="bottom-start" placement="bottom-start"

View File

@ -51,3 +51,12 @@ export const groupReactions: (reactions: any[], key: string) => { [key: string]:
return groupedReactions; return groupedReactions;
}; };
export const convertHexEmojiToDecimal = (emojiUnified: string): string => {
if (!emojiUnified) return "";
return emojiUnified
.split("-")
.map((e) => parseInt(e, 16))
.join("-");
};

View File

@ -43,3 +43,6 @@ export const orderJoinedProjects = (
return updatedSortOrder; return updatedSortOrder;
}; };
export const projectIdentifierSanitizer = (identifier: string): string =>
identifier.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, "");

View File

@ -24,8 +24,8 @@ export const ProjectSettingsSidebar = () => {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-xs font-semibold text-custom-sidebar-text-400">SETTINGS</span> <span className="text-xs font-semibold text-custom-sidebar-text-400">SETTINGS</span>
<Loader className="flex w-full flex-col gap-2"> <Loader className="flex w-full flex-col gap-2">
{[...Array(8)].map(() => ( {[...Array(8)].map((index) => (
<Loader.Item height="34px" /> <Loader.Item key={index} height="34px" />
))} ))}
</Loader> </Loader>
</div> </div>

View File

@ -6,6 +6,7 @@ import { ThemeProvider } from "next-themes";
import "styles/globals.css"; import "styles/globals.css";
import "styles/command-pallette.css"; import "styles/command-pallette.css";
import "styles/nprogress.css"; import "styles/nprogress.css";
import "styles/emoji.css";
import "styles/react-day-picker.css"; import "styles/react-day-picker.css";
// constants // constants
import { THEMES } from "constants/themes"; import { THEMES } from "constants/themes";

52
web/styles/emoji.css Normal file
View File

@ -0,0 +1,52 @@
.EmojiPickerReact {
--epr-category-navigation-button-size: 1.25rem !important;
--epr-category-label-height: 1.5rem !important;
--epr-emoji-size: 1.25rem !important;
--epr-picker-border-radius: 0.25rem !important;
--epr-horizontal-padding: 0.5rem !important;
--epr-emoji-padding: 0.5rem !important;
background-color: rgba(var(--color-background-100)) !important;
}
.epr-main {
border: none !important;
border-radius: 0 !important;
}
.epr-emoji-category-label {
font-size: 0.7875rem !important;
color: rgba(var(--color-text-300)) !important;
background-color: rgba(var(--color-background-100), 0.8) !important;
}
.epr-category-nav,
.epr-header-overlay {
padding: 0.5rem !important;
}
button.epr-emoji:hover > *,
button.epr-emoji:focus > * {
background-color: rgba(var(--color-background-80)) !important;
}
input.epr-search {
font-size: 0.7875rem !important;
height: 2rem !important;
background: transparent !important;
border-color: rgba(var(--color-border-200)) !important;
border-radius: 0.25rem !important;
}
input.epr-search::placeholder {
color: rgba(var(--color-text-400)) !important;
}
button.epr-btn-clear-search:hover {
background-color: rgba(var(--color-background-80)) !important;
color: rgba(var(--color-text-300)) !important;
}
.epr-emoji-variation-picker {
background-color: rgba(var(--color-background-100)) !important;
border-color: rgba(var(--color-border-200)) !important;
}

View File

@ -2722,7 +2722,7 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*", "@types/react@^18.2.42": "@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42":
version "18.2.42" version "18.2.42"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA== integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
@ -4064,6 +4064,11 @@ electron-to-chromium@^1.4.601:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz#4bddbc2c76e1e9dbf449ecd5da3d8119826ea4fb" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz#4bddbc2c76e1e9dbf449ecd5da3d8119826ea4fb"
integrity sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg== integrity sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==
emoji-picker-react@^4.5.16:
version "4.5.16"
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.16.tgz#12111f89a7fd2bd74965337d53806f4153d65dc6"
integrity sha512-RXaOH1EapmqbtRSMaHnwJWMfA6kiPipg/gN4cFOQRQKvrTQIA3K5+yUyzFuq8O7umIEtXUi1C1tf2dPvyyn44Q==
emoji-regex@^8.0.0: emoji-regex@^8.0.0:
version "8.0.0" version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"