forked from github/plane
fix: command palette fixes and sidebar fixes (#2482)
* fix: project fav changes * fix: project create workspace member * style: member select dropdown ui and command k modal alignment fix (#2473) * style: member select dropdown fix * style: command k modal alignment fix * fix: project create modal changes * fix: sidebar shortcut fixes * fix: minor console issues --------- Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
This commit is contained in:
parent
9b96e297b3
commit
15f621ad91
@ -255,7 +255,8 @@ export const CommandModal: React.FC<Props> = (props) => {
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-custom-border-200 divide-opacity-10 rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
|
||||
<Dialog.Panel className="relative flex items-center justify-center w-full ">
|
||||
<div className="w-full max-w-2xl transform divide-y divide-custom-border-200 divide-opacity-10 rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
|
||||
<Command
|
||||
filter={(value, search) => {
|
||||
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
|
||||
@ -754,6 +755,7 @@ export const CommandModal: React.FC<Props> = (props) => {
|
||||
{page === "change-interface-theme" && <ChangeInterfaceTheme setIsPaletteOpen={closePalette} />}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
|
@ -53,7 +53,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
isDeleteIssueModalOpen,
|
||||
toggleDeleteIssueModal,
|
||||
} = commandPalette;
|
||||
const { setSidebarCollapsed } = themeStore;
|
||||
const { toggleSidebar } = themeStore;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
@ -109,22 +109,22 @@ export const CommandPalette: FC = observer(() => {
|
||||
copyIssueUrlToClipboard();
|
||||
} else if (keyPressed === "b") {
|
||||
e.preventDefault();
|
||||
setSidebarCollapsed();
|
||||
toggleSidebar();
|
||||
}
|
||||
} else {
|
||||
if (keyPressed === "c") {
|
||||
toggleCreateIssueModal(true);
|
||||
} else if (keyPressed === "p") {
|
||||
toggleCreateProjectModal(true);
|
||||
} else if (keyPressed === "v") {
|
||||
toggleCreateViewModal(true);
|
||||
} else if (keyPressed === "d") {
|
||||
toggleCreatePageModal(true);
|
||||
} else if (keyPressed === "h") {
|
||||
toggleShortcutModal(true);
|
||||
} else if (keyPressed === "q") {
|
||||
} else if (keyPressed === "v" && workspaceSlug && projectId) {
|
||||
toggleCreateViewModal(true);
|
||||
} else if (keyPressed === "d" && workspaceSlug && projectId) {
|
||||
toggleCreatePageModal(true);
|
||||
} else if (keyPressed === "q" && workspaceSlug && projectId) {
|
||||
toggleCreateCycleModal(true);
|
||||
} else if (keyPressed === "m") {
|
||||
} else if (keyPressed === "m" && workspaceSlug && projectId) {
|
||||
toggleCreateModuleModal(true);
|
||||
} else if (keyPressed === "backspace" || keyPressed === "delete") {
|
||||
e.preventDefault();
|
||||
@ -142,8 +142,10 @@ export const CommandPalette: FC = observer(() => {
|
||||
toggleCreateModuleModal,
|
||||
toggleBulkDeleteIssueModal,
|
||||
toggleCommandPaletteModal,
|
||||
setSidebarCollapsed,
|
||||
toggleSidebar,
|
||||
toggleCreateIssueModal,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -22,8 +22,6 @@ export const ProjectCardList: FC<IProjectCardList> = observer((props) => {
|
||||
|
||||
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
|
||||
|
||||
console.log("projects", projects);
|
||||
|
||||
if (!projects) {
|
||||
return (
|
||||
<Loader className="grid grid-cols-3 gap-4">
|
||||
|
@ -1,16 +1,14 @@
|
||||
import { useState, useEffect, Fragment, FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState, useEffect, Fragment, FC, ChangeEvent } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { Users2, X } from "lucide-react";
|
||||
import { X } from "lucide-react";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useWorkspaceMyMembership } from "contexts/workspace-member.context";
|
||||
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||
// ui
|
||||
import { CustomSelect, Avatar, CustomSearchSelect } from "components/ui";
|
||||
import { CustomSelect } from "components/ui";
|
||||
import { Button, Input, TextArea } from "@plane/ui";
|
||||
// components
|
||||
import { ImagePickerPopover } from "components/core";
|
||||
@ -18,9 +16,11 @@ import EmojiIconPicker from "components/emoji-icon-picker";
|
||||
// helpers
|
||||
import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
|
||||
// types
|
||||
import { IProject } from "types";
|
||||
import { IWorkspaceMember } from "types";
|
||||
// constants
|
||||
import { NETWORK_CHOICES } from "constants/project";
|
||||
import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project";
|
||||
import { WorkspaceMemberSelect } from "components/workspace";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@ -29,17 +29,6 @@ type Props = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IProject> = {
|
||||
cover_image:
|
||||
"https://images.unsplash.com/photo-1531045535792-b515d59c3d1f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80",
|
||||
description: "",
|
||||
emoji_and_icon: getRandomEmoji(),
|
||||
identifier: "",
|
||||
name: "",
|
||||
network: 2,
|
||||
project_lead: null,
|
||||
};
|
||||
|
||||
interface IIsGuestCondition {
|
||||
onClose: () => void;
|
||||
}
|
||||
@ -59,18 +48,30 @@ const IsGuestCondition: FC<IIsGuestCondition> = ({ onClose }) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const CreateProjectModal: React.FC<Props> = (props) => {
|
||||
export interface ICreateProjectForm {
|
||||
name: string;
|
||||
identifier: string;
|
||||
description: string;
|
||||
emoji_and_icon: string;
|
||||
network: number;
|
||||
project_lead_member: IWorkspaceMember;
|
||||
project_lead: string;
|
||||
cover_image: string;
|
||||
icon_prop: any;
|
||||
emoji: string;
|
||||
}
|
||||
|
||||
export const CreateProjectModal: FC<Props> = observer((props) => {
|
||||
const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props;
|
||||
// store
|
||||
const { project: projectStore } = useMobxStore();
|
||||
const { project: projectStore, workspace: workspaceStore } = useMobxStore();
|
||||
const workspaceMembers = workspaceStore.members[workspaceSlug] || [];
|
||||
// states
|
||||
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
|
||||
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { memberDetails } = useWorkspaceMyMembership();
|
||||
const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
|
||||
|
||||
// form info
|
||||
const cover_image = PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)];
|
||||
const {
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
@ -78,15 +79,29 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
} = useForm<IProject>({
|
||||
defaultValues,
|
||||
} = useForm<ICreateProjectForm>({
|
||||
defaultValues: {
|
||||
cover_image,
|
||||
description: "",
|
||||
emoji_and_icon: getRandomEmoji(),
|
||||
identifier: "",
|
||||
name: "",
|
||||
network: 2,
|
||||
project_lead: undefined,
|
||||
},
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
const { memberDetails } = useWorkspaceMyMembership();
|
||||
|
||||
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
|
||||
|
||||
if (memberDetails && isOpen) if (memberDetails.role <= 10) return <IsGuestCondition onClose={onClose} />;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
setIsChangeInIdentifierRequired(true);
|
||||
reset(defaultValues);
|
||||
reset();
|
||||
};
|
||||
|
||||
const handleAddToFavorites = (projectId: string) => {
|
||||
@ -101,16 +116,16 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (formData: IProject) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
const onSubmit = async (formData: ICreateProjectForm) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { emoji_and_icon, ...payload } = formData;
|
||||
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;
|
||||
|
||||
await projectStore
|
||||
payload.project_lead = formData.project_lead_member?.member.id;
|
||||
|
||||
return projectStore
|
||||
.createProject(workspaceSlug.toString(), payload)
|
||||
.then((res) => {
|
||||
setToastAlert({
|
||||
@ -134,9 +149,11 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const changeIdentifierOnNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!isChangeInIdentifierRequired) return;
|
||||
|
||||
const handleNameChange = (onChange: any) => (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!isChangeInIdentifierRequired) {
|
||||
onChange(e);
|
||||
return;
|
||||
}
|
||||
if (e.target.value === "") setValue("identifier", "");
|
||||
else
|
||||
setValue(
|
||||
@ -146,32 +163,16 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
||||
.toUpperCase()
|
||||
.substring(0, 5)
|
||||
);
|
||||
onChange(e);
|
||||
};
|
||||
|
||||
const handleIdentifierChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleIdentifierChange = (onChange: any) => (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = e.target;
|
||||
|
||||
const alphanumericValue = value.replace(/[^a-zA-Z0-9]/g, "");
|
||||
|
||||
setValue("identifier", alphanumericValue.toUpperCase());
|
||||
setIsChangeInIdentifierRequired(false);
|
||||
onChange(alphanumericValue.toUpperCase());
|
||||
};
|
||||
|
||||
const options = workspaceMembers?.map((member: any) => ({
|
||||
value: member.member.id,
|
||||
query: member.member.display_name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
|
||||
|
||||
if (memberDetails && isOpen) if (memberDetails.role <= 10) return <IsGuestCondition onClose={onClose} />;
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
@ -255,20 +256,20 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
||||
message: "Title should be less than 255 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, ref } }) => (
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={changeIdentifierOnNameChange}
|
||||
ref={ref}
|
||||
onChange={handleNameChange(onChange)}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Project Title"
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs text-red-500">{errors?.name?.message}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
@ -287,20 +288,20 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
||||
message: "Identifier must at most be of 12 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, ref } }) => (
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
id="identifier"
|
||||
name="identifier"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={handleIdentifierChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
onChange={handleIdentifierChange(onChange)}
|
||||
hasError={Boolean(errors.identifier)}
|
||||
placeholder="Identifier"
|
||||
className="text-sm w-full"
|
||||
className="text-xs w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs text-red-500">{errors?.identifier?.message}</span>
|
||||
</div>
|
||||
<div className="md:col-span-4">
|
||||
<Controller
|
||||
@ -314,7 +315,7 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
||||
placeholder="Description..."
|
||||
onChange={onChange}
|
||||
className="text-sm !h-24"
|
||||
hasError={Boolean(errors?.name)}
|
||||
hasError={Boolean(errors?.description)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -330,12 +331,12 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
buttonClassName="border-[0.5px] !px-2 shadow-md"
|
||||
buttonClassName="border-[0.5px] shadow-md !py-1.5"
|
||||
label={
|
||||
<div className="flex items-center gap-2 -mb-0.5 py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{currentNetwork ? (
|
||||
<>
|
||||
<currentNetwork.icon className="h-3 w-3" />
|
||||
<currentNetwork.icon className="h-[18px] w-[18px]" />
|
||||
{currentNetwork.label}
|
||||
</>
|
||||
) : (
|
||||
@ -351,7 +352,7 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
||||
value={network.key}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<network.icon className="h-3 w-3" />
|
||||
<network.icon className="h-4 w-4" />
|
||||
{network.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
@ -361,39 +362,16 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<Controller
|
||||
name="project_lead"
|
||||
name="project_lead_member"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const selectedMember = workspaceMembers?.find((m: any) => m.member.id === value);
|
||||
|
||||
return (
|
||||
<CustomSearchSelect
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<WorkspaceMemberSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
buttonClassName="border-[0.5px] !px-2 shadow-md"
|
||||
label={
|
||||
<div className="flex items-center justify-center gap-2 py-[1px]">
|
||||
{value ? (
|
||||
<>
|
||||
<Avatar user={selectedMember?.member} />
|
||||
<span>{selectedMember?.member.display_name} </span>
|
||||
<span onClick={() => onChange(null)}>
|
||||
<X className="h-3 w-3 -mb-0.5 text-custom-text-200 hover:text-custom-text-100" />
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Users2 className="h-3.5 w-3.5 text-custom-text-400" />
|
||||
<span className="text-custom-text-400">Lead</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
noChevron
|
||||
options={workspaceMembers}
|
||||
placeholder="Select Lead"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -415,4 +393,4 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -18,12 +18,20 @@ type AvatarProps = {
|
||||
height?: string;
|
||||
width?: string;
|
||||
fontSize?: string;
|
||||
showName?: boolean;
|
||||
};
|
||||
|
||||
// services
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
export const Avatar: React.FC<AvatarProps> = ({ user, index, height = "24px", width = "24px", fontSize = "12px" }) => (
|
||||
export const Avatar: React.FC<AvatarProps> = ({
|
||||
user,
|
||||
index,
|
||||
height = "24px",
|
||||
width = "24px",
|
||||
fontSize = "12px",
|
||||
showName,
|
||||
}) => (
|
||||
<div
|
||||
className={`relative rounded border-[0.5px] ${
|
||||
index && index !== 0 ? "-ml-3.5 border-custom-border-200" : "border-transparent"
|
||||
@ -61,6 +69,7 @@ export const Avatar: React.FC<AvatarProps> = ({ user, index, height = "24px", wi
|
||||
{user?.display_name?.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
{showName && <span>{user?.display_name ? user?.display_name : user?.first_name}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -92,7 +92,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none md:hidden"
|
||||
onClick={() => themeStore.setSidebarCollapsed(!isCollapsed)}
|
||||
onClick={() => themeStore.toggleSidebar()}
|
||||
>
|
||||
<MoveLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@ -101,7 +101,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
||||
className={`hidden md:grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
||||
isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => themeStore.setSidebarCollapsed(!isCollapsed)}
|
||||
onClick={() => themeStore.toggleSidebar()}
|
||||
>
|
||||
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isCollapsed ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
|
@ -10,3 +10,4 @@ export * from "./issues-stats";
|
||||
export * from "./sidebar-dropdown";
|
||||
export * from "./sidebar-menu";
|
||||
export * from "./sidebar-quick-action";
|
||||
export * from "./member-select";
|
||||
|
146
web/components/workspace/member-select.tsx
Normal file
146
web/components/workspace/member-select.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import React, { FC, useState, Fragment } from "react";
|
||||
// popper js
|
||||
import { usePopper } from "react-popper";
|
||||
// ui
|
||||
import { Input, Tooltip } from "@plane/ui";
|
||||
import { Listbox } from "@headlessui/react";
|
||||
import { Avatar } from "components/ui";
|
||||
// icons
|
||||
import { Check, Search, User2 } from "lucide-react";
|
||||
// types
|
||||
import { IWorkspaceMember } from "types";
|
||||
|
||||
export interface IWorkspaceMemberSelect {
|
||||
value: IWorkspaceMember | undefined;
|
||||
onChange: (value: IWorkspaceMember) => void;
|
||||
options: IWorkspaceMember[];
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const WorkspaceMemberSelect: FC<IWorkspaceMemberSelect> = (props) => {
|
||||
const { value, onChange, options, placeholder = "Select Member", disabled = false } = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "auto",
|
||||
});
|
||||
|
||||
// const options = workspaceMembers?.map((member: any) => ({
|
||||
// value: member.member.id,
|
||||
// query: member.member.display_name,
|
||||
// content: (
|
||||
// <div className="flex items-center gap-2">
|
||||
// <Avatar user={member.member} />
|
||||
// {member.member.display_name}
|
||||
// </div>
|
||||
// ),
|
||||
// }));
|
||||
|
||||
// const selectedOption = workspaceMembers?.find((member) => member.member.id === value);
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? options
|
||||
: options?.filter((option) => option.member.display_name.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const label = (
|
||||
<Tooltip
|
||||
tooltipHeading="Assignee"
|
||||
tooltipContent={
|
||||
options && options.length > 0
|
||||
? options.map((assignee) => assignee?.member.display_name).join(", ")
|
||||
: "No Assignee"
|
||||
}
|
||||
position="top"
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-between gap-2 w-full text-xs px-2.5 py-1.5 rounded-md border border-custom-border-300 duration-300 focus:outline-none
|
||||
"
|
||||
>
|
||||
{value ? (
|
||||
<>
|
||||
<Avatar height="18px" width="18px" user={value?.member} />
|
||||
<span className="text-xs leading-4"> {value?.member.display_name}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<User2 className="h-[18px] w-[18px]" />
|
||||
<span className="text-xs leading-4">{placeholder}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<Listbox as="div" className={`flex-shrink-0 text-left`} value={value} onChange={onChange} disabled={disabled}>
|
||||
<Listbox.Button as={React.Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} `}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</Listbox.Button>
|
||||
<Listbox.Options>
|
||||
<div
|
||||
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 border-none focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((workspaceMember: IWorkspaceMember) => (
|
||||
<Listbox.Option
|
||||
key={workspaceMember.id}
|
||||
value={workspaceMember}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active && !selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={workspaceMember.member} />
|
||||
{workspaceMember.member.display_name}
|
||||
</div>
|
||||
{selected && <Check className="h-3.5 w-3.5" />}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Listbox>
|
||||
);
|
||||
};
|
@ -50,3 +50,22 @@ export const PROJECT_AUTOMATION_MONTHS = [
|
||||
{ label: "9 Months", value: 9 },
|
||||
{ label: "12 Months", value: 12 },
|
||||
];
|
||||
|
||||
export const PROJECT_UNSPLASH_COVERS = [
|
||||
"https://images.unsplash.com/photo-1531045535792-b515d59c3d1f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1693027407934-e3aa8a54c7ae?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1518837695005-2083093ee35b?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1464925257126-6450e871c667?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1606768666853-403c90a981ad?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1627556592933-ffe99c1cd9eb?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1643330683233-ff2ac89b002c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1542202229-7d93c33f5d07?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1511497584788-876760111969?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1475738972911-5b44ce984c42?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1673393058808-50e9baaf4d2c?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1696643830146-44a8755f1905?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1693868769698-6c7440636a09?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1691230995681-480d86cbc135?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80",
|
||||
"https://images.unsplash.com/photo-1675351066828-6fc770b90dd2?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80",
|
||||
];
|
||||
|
@ -30,14 +30,11 @@ const MobxStoreInit = observer(() => {
|
||||
|
||||
useEffect(() => {
|
||||
// sidebar collapsed toggle
|
||||
if (localStorage && localStorage.getItem("app_sidebar_collapsed") && themeStore?.sidebarCollapsed === null)
|
||||
themeStore.setSidebarCollapsed(
|
||||
localStorage.getItem("app_sidebar_collapsed")
|
||||
? localStorage.getItem("app_sidebar_collapsed") === "true"
|
||||
? true
|
||||
: false
|
||||
: false
|
||||
);
|
||||
const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed");
|
||||
const localBoolValue = localValue ? (localValue === "true" ? true : false) : false;
|
||||
if (localValue && themeStore?.sidebarCollapsed === undefined) {
|
||||
themeStore.toggleSidebar(localBoolValue);
|
||||
}
|
||||
}, [themeStore, userStore, setTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -68,7 +68,7 @@ export interface IProjectStore {
|
||||
joinProject: (workspaceSlug: string, projectIds: string[]) => Promise<void>;
|
||||
leaveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
createProject: (workspaceSlug: string, data: any) => Promise<any>;
|
||||
updateProject: (workspaceSlug: string, projectId: string, data: any) => Promise<any>;
|
||||
updateProject: (workspaceSlug: string, projectId: string, data: Partial<IProject>) => Promise<any>;
|
||||
deleteProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@ -413,17 +413,39 @@ export class ProjectStore implements IProjectStore {
|
||||
|
||||
addProjectToFavorites = async (workspaceSlug: string, projectId: string) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.projects = {
|
||||
...this.projects,
|
||||
[workspaceSlug]: this.projects[workspaceSlug].map((project) => {
|
||||
if (project.id === projectId) {
|
||||
return { ...project, is_favorite: true };
|
||||
}
|
||||
return project;
|
||||
}),
|
||||
};
|
||||
});
|
||||
const response = await this.projectService.addProjectToFavorites(workspaceSlug, projectId);
|
||||
await this.fetchProjects(workspaceSlug);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.log("Failed to add project to favorite");
|
||||
await this.fetchProjects(workspaceSlug);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
removeProjectFromFavorites = async (workspaceSlug: string, projectId: string) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.projects = {
|
||||
...this.projects,
|
||||
[workspaceSlug]: this.projects[workspaceSlug].map((project) => {
|
||||
if (project.id === projectId) {
|
||||
return { ...project, is_favorite: false };
|
||||
}
|
||||
return project;
|
||||
}),
|
||||
};
|
||||
});
|
||||
const response = await this.projectService.removeProjectFromFavorites(workspaceSlug, projectId);
|
||||
await this.fetchProjects(workspaceSlug);
|
||||
return response;
|
||||
@ -546,19 +568,26 @@ export class ProjectStore implements IProjectStore {
|
||||
}
|
||||
};
|
||||
|
||||
updateProject = async (workspaceSlug: string, projectId: string, data: any) => {
|
||||
updateProject = async (workspaceSlug: string, projectId: string, data: Partial<IProject>) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.projects = {
|
||||
...this.projects,
|
||||
[workspaceSlug]: this.projects[workspaceSlug].map((p) => (p.id === projectId ? { ...p, ...data } : p)),
|
||||
};
|
||||
});
|
||||
|
||||
const response = await this.projectService.updateProject(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
data,
|
||||
this.rootStore.user.currentUser
|
||||
);
|
||||
await this.fetchProjectDetails(workspaceSlug, projectId);
|
||||
await this.fetchProjects(workspaceSlug);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.log("Failed to create project from project store");
|
||||
|
||||
this.fetchProjects(workspaceSlug);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
@ -5,14 +5,14 @@ import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper";
|
||||
|
||||
export interface IThemeStore {
|
||||
theme: string | null;
|
||||
sidebarCollapsed: boolean | null;
|
||||
sidebarCollapsed: boolean | undefined;
|
||||
|
||||
setSidebarCollapsed: (collapsed?: boolean) => void;
|
||||
toggleSidebar: (collapsed?: boolean) => void;
|
||||
setTheme: (theme: any) => void;
|
||||
}
|
||||
|
||||
class ThemeStore implements IThemeStore {
|
||||
sidebarCollapsed: boolean | null = null;
|
||||
sidebarCollapsed: boolean | undefined = undefined;
|
||||
theme: string | null = null;
|
||||
// root store
|
||||
rootStore;
|
||||
@ -23,7 +23,7 @@ class ThemeStore implements IThemeStore {
|
||||
sidebarCollapsed: observable.ref,
|
||||
theme: observable.ref,
|
||||
// action
|
||||
setSidebarCollapsed: action,
|
||||
toggleSidebar: action,
|
||||
setTheme: action,
|
||||
// computed
|
||||
});
|
||||
@ -31,17 +31,14 @@ class ThemeStore implements IThemeStore {
|
||||
this.rootStore = _rootStore;
|
||||
this.initialLoad();
|
||||
}
|
||||
|
||||
setSidebarCollapsed(collapsed?: boolean) {
|
||||
if (!collapsed) {
|
||||
let _sidebarCollapsed: string | boolean | null = localStorage.getItem("app_sidebar_collapsed");
|
||||
_sidebarCollapsed = _sidebarCollapsed ? (_sidebarCollapsed === "true" ? true : false) : false;
|
||||
this.sidebarCollapsed = _sidebarCollapsed;
|
||||
toggleSidebar = (collapsed?: boolean) => {
|
||||
if (collapsed === undefined) {
|
||||
this.sidebarCollapsed = !this.sidebarCollapsed;
|
||||
} else {
|
||||
this.sidebarCollapsed = collapsed;
|
||||
localStorage.setItem("app_sidebar_collapsed", collapsed.toString());
|
||||
}
|
||||
}
|
||||
localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString());
|
||||
};
|
||||
|
||||
setTheme = async (_theme: { theme: any }) => {
|
||||
try {
|
||||
|
@ -15,8 +15,8 @@ export interface IWorkspaceStore {
|
||||
// observables
|
||||
workspaceSlug: string | null;
|
||||
workspaces: IWorkspace[];
|
||||
labels: { [workspaceSlug: string]: IIssueLabels[] } | {}; // workspaceSlug: labels[]
|
||||
members: { [workspaceSlug: string]: IWorkspaceMember[] } | {}; // workspaceSlug: members[]
|
||||
labels: { [workspaceSlug: string]: IIssueLabels[] }; // workspaceSlug: labels[]
|
||||
members: { [workspaceSlug: string]: IWorkspaceMember[] }; // workspaceSlug: members[]
|
||||
|
||||
// actions
|
||||
setWorkspaceSlug: (workspaceSlug: string) => void;
|
||||
|
Loading…
Reference in New Issue
Block a user