style: project setting ui revamp (#2177)

* style: project settings navigation sidebar added

* chore: emoji and image picker close on outside click added

* style: project setting general page revamp

* style: project setting member page revamp

* style: project setting features page revamp

* style: project setting state page revamp

* style: project setting integrations page revamp

* style: project setting estimates page revamp

* style: project setting automation page revamp

* style: project setting label page revamp

* chore: member select improvement for member setting page

* chore: toggle switch component improvement

* style: project automation setting ui improvement

* style: module icon added

* style: toggle switch improvement

* style: ui and spacing consistency

* style: project label setting revamp

* style: project state setting ui improvement

* chore: integration setting repo select validation

* chore: code refactor

* fix: build fix
This commit is contained in:
Anmol Singh Bhatia 2023-09-13 23:09:55 +05:30 committed by GitHub
parent d0f6ca3bac
commit 87abf3ccb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1090 additions and 876 deletions

View File

@ -3,8 +3,8 @@ import React, { useState } from "react";
// component // component
import { CustomSelect, ToggleSwitch } from "components/ui"; import { CustomSelect, ToggleSwitch } from "components/ui";
import { SelectMonthModal } from "components/automation"; import { SelectMonthModal } from "components/automation";
// icons // icon
import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { ArchiveRestore } from "lucide-react";
// constants // constants
import { PROJECT_AUTOMATION_MONTHS } from "constants/project"; import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
// types // types
@ -28,15 +28,19 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
handleClose={() => setmonthModal(false)} handleClose={() => setmonthModal(false)}
handleChange={handleChange} handleChange={handleChange}
/> />
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-300 bg-custom-background-90"> <div className="flex flex-col gap-4 border-b border-custom-border-200 px-4 py-6">
<div className="flex items-center justify-between gap-x-8 gap-y-2"> <div className="flex items-center justify-between">
<div className="flex flex-col gap-2.5"> <div className="flex items-start gap-3">
<h4 className="text-lg font-semibold">Auto-archive closed issues</h4> <div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
<p className="text-sm text-custom-text-200"> <ArchiveRestore className="h-4 w-4 text-custom-text-100 flex-shrink-0" />
Plane will automatically archive issues that have been completed or cancelled for the </div>
configured time period. <div className="">
<h4 className="text-sm font-medium">Auto-archive closed issues</h4>
<p className="text-sm text-custom-text-200 tracking-tight">
Plane will auto archive issues that have been completed or canceled.
</p> </p>
</div> </div>
</div>
<ToggleSwitch <ToggleSwitch
value={projectDetails?.archive_in !== 0} value={projectDetails?.archive_in !== 0}
onChange={() => onChange={() =>
@ -47,9 +51,11 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
size="sm" size="sm"
/> />
</div> </div>
{projectDetails?.archive_in !== 0 && ( {projectDetails?.archive_in !== 0 && (
<div className="flex items-center justify-between gap-2 w-full"> <div className="ml-12">
<div className="w-1/2 text-base font-medium"> <div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full">
<div className="w-1/2 text-sm font-medium">
Auto-archive issues that are closed for Auto-archive issues that are closed for
</div> </div>
<div className="w-1/2"> <div className="w-1/2">
@ -62,19 +68,19 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
handleChange({ archive_in: val }); handleChange({ archive_in: val });
}} }}
input input
verticalPosition="top" verticalPosition="bottom"
width="w-full" width="w-full"
> >
<> <>
{PROJECT_AUTOMATION_MONTHS.map((month) => ( {PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.label} value={month.value}> <CustomSelect.Option key={month.label} value={month.value}>
{month.label} <span className="text-sm">{month.label}</span>
</CustomSelect.Option> </CustomSelect.Option>
))} ))}
<button <button
type="button" type="button"
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80" className="flex w-full text-sm select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
onClick={() => setmonthModal(true)} onClick={() => setmonthModal(true)}
> >
Customise Time Range Customise Time Range
@ -83,6 +89,7 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
</CustomSelect> </CustomSelect>
</div> </div>
</div> </div>
</div>
)} )}
</div> </div>
</> </>

View File

@ -5,11 +5,12 @@ import useSWR from "swr";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// component // component
import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui"; import { CustomSearchSelect, CustomSelect, Icon, ToggleSwitch } from "components/ui";
import { SelectMonthModal } from "components/automation"; import { SelectMonthModal } from "components/automation";
// icons // icons
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; import { Squares2X2Icon } from "@heroicons/react/24/outline";
import { StateGroupIcon } from "components/icons"; import { StateGroupIcon } from "components/icons";
import { ArchiveX } from "lucide-react";
// services // services
import stateService from "services/state.service"; import stateService from "services/state.service";
// constants // constants
@ -76,15 +77,19 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
handleChange={handleChange} handleChange={handleChange}
/> />
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-300 bg-custom-background-90"> <div className="flex flex-col gap-4 border-b border-custom-border-200 px-4 py-6">
<div className="flex items-center justify-between gap-x-8 gap-y-2 "> <div className="flex items-center justify-between">
<div className="flex flex-col gap-2.5"> <div className="flex items-start gap-3">
<h4 className="text-lg font-semibold">Auto-close inactive issues</h4> <div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
<p className="text-sm text-custom-text-200"> <ArchiveX className="h-4 w-4 text-red-500 flex-shrink-0" />
Plane will automatically close the issues that have not been updated for the </div>
configured time period. <div className="">
<h4 className="text-sm font-medium">Auto-close issues</h4>
<p className="text-sm text-custom-text-200 tracking-tight">
Plane will automatically close issue that havent been completed or canceled.
</p> </p>
</div> </div>
</div>
<ToggleSwitch <ToggleSwitch
value={projectDetails?.close_in !== 0} value={projectDetails?.close_in !== 0}
onChange={() => onChange={() =>
@ -95,10 +100,12 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
size="sm" size="sm"
/> />
</div> </div>
{projectDetails?.close_in !== 0 && ( {projectDetails?.close_in !== 0 && (
<div className="flex flex-col gap-4 w-full"> <div className="ml-12">
<div className="flex items-center justify-between gap-2 w-full"> <div className="flex flex-col gap-4">
<div className="w-1/2 text-base font-medium"> <div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full">
<div className="w-1/2 text-sm font-medium">
Auto-close issues that are inactive for Auto-close issues that are inactive for
</div> </div>
<div className="w-1/2"> <div className="w-1/2">
@ -130,8 +137,9 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
</CustomSelect> </CustomSelect>
</div> </div>
</div> </div>
<div className="flex items-center justify-between gap-2 w-full">
<div className="w-1/2 text-base font-medium">Auto-close Status</div> <div className="flex items-center justify-between rounded px-5 py-4 bg-custom-background-90 border border-custom-border-200 gap-2 w-full">
<div className="w-1/2 text-sm font-medium">Auto-close Status</div>
<div className="w-1/2 "> <div className="w-1/2 ">
<CustomSearchSelect <CustomSearchSelect
value={ value={
@ -174,6 +182,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
</div> </div>
</div> </div>
</div> </div>
</div>
)} )}
</div> </div>
</> </>

View File

@ -20,6 +20,7 @@ import fileService from "services/file.service";
import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui"; import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui";
// hooks // hooks
import useWorkspaceDetails from "hooks/use-workspace-details"; import useWorkspaceDetails from "hooks/use-workspace-details";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
const unsplashEnabled = const unsplashEnabled =
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" || process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" ||
@ -67,6 +68,8 @@ export const ImagePickerPopover: React.FC<Props> = ({
fileService.getUnsplashImages(1, searchParams) fileService.getUnsplashImages(1, searchParams)
); );
const imagePickerRef = useRef<HTMLDivElement>(null);
const { workspaceDetails } = useWorkspaceDetails(); const { workspaceDetails } = useWorkspaceDetails();
const onDrop = useCallback((acceptedFiles: File[]) => { const onDrop = useCallback((acceptedFiles: File[]) => {
@ -116,12 +119,14 @@ export const ImagePickerPopover: React.FC<Props> = ({
onChange(images[0].urls.regular); onChange(images[0].urls.regular);
}, [value, onChange, images]); }, [value, onChange, images]);
useOutsideClickDetector(imagePickerRef, () => setIsOpen(false));
if (!unsplashEnabled) return null; if (!unsplashEnabled) return null;
return ( return (
<Popover className="relative z-[2]" ref={ref}> <Popover className="relative z-[2]" ref={ref}>
<Popover.Button <Popover.Button
className="rounded-md border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100" className="rounded-sm border border-custom-border-300 bg-custom-background-100 px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100"
onClick={() => setIsOpen((prev) => !prev)} onClick={() => setIsOpen((prev) => !prev)}
disabled={disabled} disabled={disabled}
> >
@ -137,7 +142,10 @@ export const ImagePickerPopover: React.FC<Props> = ({
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg"> <Popover.Panel className="absolute right-0 z-10 mt-2 rounded-md border border-custom-border-200 bg-custom-background-80 shadow-lg">
<div className="h-96 md:h-[28rem] w-80 md:w-[36rem] flex flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl"> <div
ref={imagePickerRef}
className="h-96 md:h-[28rem] w-80 md:w-[36rem] flex flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl"
>
<Tab.Group> <Tab.Group>
<div> <div>
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1"> <Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">

View File

@ -1,8 +1,10 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, useRef } from "react";
// headless ui // headless ui
import { Tab, Transition, Popover } from "@headlessui/react"; import { Tab, Transition, Popover } from "@headlessui/react";
// react colors // react colors
import { TwitterPicker } from "react-color"; import { TwitterPicker } from "react-color";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// types // types
import { Props } from "./types"; import { Props } from "./types";
// emojis // emojis
@ -36,6 +38,8 @@ const EmojiIconPicker: React.FC<Props> = ({
const [recentEmojis, setRecentEmojis] = useState<string[]>([]); const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
const emojiPickerRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
setRecentEmojis(getRecentEmojis()); setRecentEmojis(getRecentEmojis());
}, []); }, []);
@ -44,6 +48,8 @@ const EmojiIconPicker: React.FC<Props> = ({
if (!value || value?.length === 0) onChange(getRandomEmoji()); if (!value || value?.length === 0) onChange(getRandomEmoji());
}, [value, onChange]); }, [value, onChange]);
useOutsideClickDetector(emojiPickerRef, () => setIsOpen(false));
return ( return (
<Popover className="relative z-[1]"> <Popover className="relative z-[1]">
<Popover.Button <Popover.Button
@ -63,7 +69,10 @@ const EmojiIconPicker: React.FC<Props> = ({
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Popover.Panel className="absolute z-10 mt-2 w-[250px] rounded-[4px] border border-custom-border-200 bg-custom-background-80 shadow-lg"> <Popover.Panel className="absolute 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"> <div
ref={emojiPickerRef}
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.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"> <Tab.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
{tabOptions.map((tab) => ( {tabOptions.map((tab) => (

View File

@ -66,7 +66,7 @@ export const SingleEstimate: React.FC<Props> = ({
return ( return (
<> <>
<div className="gap-2 py-3"> <div className="gap-2 p-4 border-b border-custom-border-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h6 className="flex w-[40vw] items-center gap-2 truncate text-sm font-medium"> <h6 className="flex w-[40vw] items-center gap-2 truncate text-sm font-medium">

View File

@ -84,3 +84,4 @@ export * from "./clock-icon";
export * from "./bell-icon"; export * from "./bell-icon";
export * from "./single-comment-icon"; export * from "./single-comment-icon";
export * from "./related-icon"; export * from "./related-icon";
export * from "./module-icon";

View File

@ -0,0 +1,59 @@
import React from "react";
import type { Props } from "./types";
export const ModuleIcon: React.FC<Props> = ({
width = "24",
height = "24",
className,
color = "#F15B5B",
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z"
stroke="#F15B5B"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.84925 4.66667H4.81221C4.73039 4.66667 4.66406 4.733 4.66406 4.81482V5.85185C4.66406 5.93367 4.73039 6 4.81221 6H5.84925C5.93107 6 5.9974 5.93367 5.9974 5.85185V4.81482C5.9974 4.733 5.93107 4.66667 5.84925 4.66667Z"
fill={color}
stroke="#F15B5B"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.84925 10H4.81221C4.73039 10 4.66406 10.0663 4.66406 10.1481V11.1852C4.66406 11.267 4.73039 11.3333 4.81221 11.3333H5.84925C5.93107 11.3333 5.9974 11.267 5.9974 11.1852V10.1481C5.9974 10.0663 5.93107 10 5.84925 10Z"
fill={color}
stroke="#F15B5B"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M11.1852 4.66667H10.1481C10.0663 4.66667 10 4.733 10 4.81482V5.85185C10 5.93367 10.0663 6 10.1481 6H11.1852C11.267 6 11.3333 5.93367 11.3333 5.85185V4.81482C11.3333 4.733 11.267 4.66667 11.1852 4.66667Z"
fill={color}
stroke="#F15B5B"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M11.1852 10H10.1481C10.0663 10 10 10.0663 10 10.1481V11.1852C10 11.267 10.0663 11.3333 10.1481 11.3333H11.1852C11.267 11.3333 11.3333 11.267 11.3333 11.1852V10.1481C11.3333 10.0663 11.267 10 11.1852 10Z"
fill={color}
stroke="#F15B5B"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);

View File

@ -66,6 +66,8 @@ export const SelectRepository: React.FC<Props> = ({
content: <p>{truncateText(repo.full_name, characterLimit)}</p>, content: <p>{truncateText(repo.full_name, characterLimit)}</p>,
})) ?? []; })) ?? [];
if (userRepositories.length < 1) return null;
return ( return (
<CustomSearchSelect <CustomSearchSelect
value={value} value={value}

View File

@ -83,9 +83,7 @@ export const SelectChannel: React.FC<Props> = ({ integration }) => {
{projectIntegration ? ( {projectIntegration ? (
<button <button
type="button" type="button"
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${ className={`relative inline-flex h-4 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none bg-gray-700`}
slackChannelAvailabilityToggle ? "bg-green-500" : "bg-gray-200"
}`}
role="switch" role="switch"
aria-checked aria-checked
onClick={() => { onClick={() => {
@ -94,8 +92,8 @@ export const SelectChannel: React.FC<Props> = ({ integration }) => {
> >
<span <span
aria-hidden="true" aria-hidden="true"
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${ className={`self-center inline-block h-2 w-2 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
slackChannelAvailabilityToggle ? "translate-x-5" : "translate-x-0" slackChannelAvailabilityToggle ? "translate-x-3" : "translate-x-0"
}`} }`}
/> />
</button> </button>

View File

@ -17,7 +17,7 @@ import issuesService from "services/issues.service";
// ui // ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui"; import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// icons // icons
import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { Component } from "lucide-react";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabels } from "types";
// fetch-keys // fetch-keys
@ -132,7 +132,7 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
return ( return (
<div <div
className={`flex scroll-m-8 items-center gap-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 ${ className={`flex scroll-m-8 items-center gap-2 rounded border border-custom-border-200 bg-custom-background-100 px-3.5 py-2 ${
labelForm ? "" : "hidden" labelForm ? "" : "hidden"
}`} }`}
ref={ref} ref={ref}
@ -146,18 +146,12 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
open ? "text-custom-text-100" : "text-custom-text-200" open ? "text-custom-text-100" : "text-custom-text-200"
}`} }`}
> >
<span <Component
className="h-5 w-5 rounded" className="h-4 w-4 text-custom-text-100 flex-shrink-0"
style={{ style={{
backgroundColor: watch("color"), color: watch("color"),
}} }}
/> />
<ChevronDownIcon
className={`ml-2 h-5 w-5 group-hover:text-custom-text-200 ${
open ? "text-gray-600" : "text-gray-400"
}`}
aria-hidden="true"
/>
</Popover.Button> </Popover.Button>
<Transition <Transition

View File

@ -13,12 +13,12 @@ import { CustomMenu } from "components/ui";
// icons // icons
import { import {
ChevronDownIcon, ChevronDownIcon,
RectangleGroupIcon,
XMarkIcon, XMarkIcon,
PlusIcon, PlusIcon,
PencilIcon, PencilIcon,
TrashIcon, TrashIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { Component, X } from "lucide-react";
// types // types
import { ICurrentUserResponse, IIssueLabels } from "types"; import { ICurrentUserResponse, IIssueLabels } from "types";
// fetch-keys // fetch-keys
@ -76,20 +76,18 @@ export const SingleLabelGroup: React.FC<Props> = ({
return ( return (
<Disclosure <Disclosure
as="div" as="div"
className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5 text-custom-text-100" className="rounded border border-custom-border-200 bg-custom-background-100 px-3.5 py-3 text-custom-text-100"
defaultOpen defaultOpen
> >
{({ open }) => ( {({ open }) => (
<> <>
<div className="flex cursor-pointer items-center justify-between gap-2"> <div className="flex cursor-pointer items-center justify-between gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span> <Component className="h-4 w-4 text-custom-text-100 flex-shrink-0" />
<RectangleGroupIcon className="h-4 w-4" />
</span>
<h6>{label.name}</h6> <h6>{label.name}</h6>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CustomMenu ellipsis> <CustomMenu ellipsis buttonClassName="!text-custom-sidebar-text-400">
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}> <CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
@ -112,7 +110,9 @@ export const SingleLabelGroup: React.FC<Props> = ({
<Disclosure.Button> <Disclosure.Button>
<span> <span>
<ChevronDownIcon <ChevronDownIcon
className={`h-4 w-4 text-custom-text-100 ${!open ? "rotate-90 transform" : ""}`} className={`h-4 w-4 text-custom-sidebar-text-400 ${
!open ? "rotate-90 transform" : ""
}`}
/> />
</span> </span>
</Disclosure.Button> </Disclosure.Button>
@ -128,15 +128,15 @@ export const SingleLabelGroup: React.FC<Props> = ({
leaveTo="transform opacity-0" leaveTo="transform opacity-0"
> >
<Disclosure.Panel> <Disclosure.Panel>
<div className="mt-3 ml-6 space-y-3"> <div className="mt-2.5 ml-6">
{labelChildren.map((child) => ( {labelChildren.map((child) => (
<div <div
key={child.id} key={child.id}
className="group flex items-center justify-between rounded-md border border-custom-border-200 p-2 text-sm" className="group flex items-center justify-between border-b border-custom-border-200 px-4 py-2.5 text-sm last:border-0"
> >
<h5 className="flex items-center gap-3"> <h5 className="flex items-center gap-3">
<span <span
className="h-2.5 w-2.5 flex-shrink-0 rounded-full" className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
style={{ style={{
backgroundColor: backgroundColor:
child.color && child.color !== "" ? child.color : "#000000", child.color && child.color !== "" ? child.color : "#000000",
@ -144,8 +144,15 @@ export const SingleLabelGroup: React.FC<Props> = ({
/> />
{child.name} {child.name}
</h5> </h5>
<div className="pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100"> <div className="flex items-center gap-3.5 pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
<CustomMenu ellipsis> <div className="h-4 w-4">
<CustomMenu
customButton={
<div className="h-4 w-4">
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
</div>
}
>
<CustomMenu.MenuItem onClick={() => removeFromGroup(child)}> <CustomMenu.MenuItem onClick={() => removeFromGroup(child)}>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" /> <XMarkIcon className="h-4 w-4" />
@ -158,14 +165,18 @@ export const SingleLabelGroup: React.FC<Props> = ({
<span>Edit label</span> <span>Edit label</span>
</span> </span>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleLabelDelete}>
<span className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete label</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu> </CustomMenu>
</div> </div>
<div className="flex items-center">
<button
className="flex items-center justify-start gap-2"
onClick={handleLabelDelete}
>
<X className="h-[18px] w-[18px] text-custom-sidebar-text-400 flex-shrink-0" />
</button>
</div>
</div>
</div> </div>
))} ))}
</div> </div>

View File

@ -5,7 +5,8 @@ import { CustomMenu } from "components/ui";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabels } from "types";
//icons //icons
import { RectangleGroupIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; import { RectangleGroupIcon, PencilIcon } from "@heroicons/react/24/outline";
import { Component, X } from "lucide-react";
type Props = { type Props = {
label: IIssueLabels; label: IIssueLabels;
@ -20,8 +21,8 @@ export const SingleLabel: React.FC<Props> = ({
editLabel, editLabel,
handleLabelDelete, handleLabelDelete,
}) => ( }) => (
<div className="gap-2 space-y-3 divide-y divide-custom-border-200 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5"> <div className="gap-2 space-y-3 divide-y divide-custom-border-200 rounded border border-custom-border-200 bg-custom-background-100 px-4 py-2.5">
<div className="flex items-center justify-between"> <div className="group flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span <span
className="h-3.5 w-3.5 flex-shrink-0 rounded-full" className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
@ -31,7 +32,15 @@ export const SingleLabel: React.FC<Props> = ({
/> />
<h6 className="text-sm">{label.name}</h6> <h6 className="text-sm">{label.name}</h6>
</div> </div>
<CustomMenu ellipsis> <div className="flex items-center gap-3.5 pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
<div className="h-4 w-4">
<CustomMenu
customButton={
<div className="h-4 w-4">
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
</div>
}
>
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}> <CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<RectangleGroupIcon className="h-4 w-4" /> <RectangleGroupIcon className="h-4 w-4" />
@ -44,13 +53,15 @@ export const SingleLabel: React.FC<Props> = ({
<span>Edit label</span> <span>Edit label</span>
</span> </span>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleLabelDelete}>
<span className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete label</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu> </CustomMenu>
</div> </div>
<div className="flex items-center">
<button className="flex items-center justify-start gap-2" onClick={handleLabelDelete}>
<X className="h-[18px] w-[18px] text-custom-sidebar-text-400 flex-shrink-0" />
</button>
</div>
</div>
</div>
</div> </div>
); );

View File

@ -1,8 +1,9 @@
export * from "./create-project-modal"; export * from "./create-project-modal";
export * from "./delete-project-modal"; export * from "./delete-project-modal";
export * from "./sidebar-list"; export * from "./sidebar-list";
export * from "./settings-header"; export * from "./settings-sidebar";
export * from "./single-integration-card"; export * from "./single-integration-card";
export * from "./single-project-card"; export * from "./single-project-card";
export * from "./single-sidebar-project"; export * from "./single-sidebar-project";
export * from "./confirm-project-leave-modal"; export * from "./confirm-project-leave-modal";
export * from "./member-select";

View File

@ -0,0 +1,74 @@
import React from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import projectService from "services/project.service";
// ui
import { Avatar, CustomSearchSelect } from "components/ui";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = {
value: any;
onChange: (val: string) => void;
};
export const MemberSelect: React.FC<Props> = ({ value, onChange }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const options = members?.map((member) => ({
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 = members?.find((m) => m.member.id === value)?.member;
return (
<CustomSearchSelect
value={value}
label={
<div className="flex items-center gap-2">
{selectedOption && <Avatar user={selectedOption} />}
{selectedOption ? (
selectedOption?.display_name
) : (
<span className="text-sm py-0.5 text-custom-text-200">Select</span>
)}
</div>
}
buttonClassName="!px-3 !py-2"
options={
options &&
options && [
...options,
{
value: "none",
query: "none",
content: <div className="flex items-center gap-2">None</div>,
},
]
}
maxHeight="md"
position="right"
width="w-full"
onChange={onChange}
/>
);
};

View File

@ -1,13 +0,0 @@
import SettingsNavbar from "layouts/settings-navbar";
export const SettingsHeader = () => (
<div className="mb-8 space-y-6">
<div>
<h3 className="text-2xl font-semibold">Project Settings</h3>
<p className="mt-1 text-sm text-custom-text-200">
This information will be displayed to every member of the project.
</p>
</div>
<SettingsNavbar />
</div>
);

View File

@ -0,0 +1,72 @@
import React from "react";
import { useRouter } from "next/router";
import Link from "next/link";
export const SettingsSidebar = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const projectLinks: Array<{
label: string;
href: string;
}> = [
{
label: "General",
href: `/${workspaceSlug}/projects/${projectId}/settings`,
},
{
label: "Members",
href: `/${workspaceSlug}/projects/${projectId}/settings/members`,
},
{
label: "Features",
href: `/${workspaceSlug}/projects/${projectId}/settings/features`,
},
{
label: "States",
href: `/${workspaceSlug}/projects/${projectId}/settings/states`,
},
{
label: "Labels",
href: `/${workspaceSlug}/projects/${projectId}/settings/labels`,
},
{
label: "Integrations",
href: `/${workspaceSlug}/projects/${projectId}/settings/integrations`,
},
{
label: "Estimates",
href: `/${workspaceSlug}/projects/${projectId}/settings/estimates`,
},
{
label: "Automations",
href: `/${workspaceSlug}/projects/${projectId}/settings/automations`,
},
];
return (
<div className="flex flex-col gap-2 w-80 px-9">
<span className="text-xs text-custom-sidebar-text-400 font-semibold">SETTINGS</span>
<div className="flex flex-col gap-1 w-full">
{projectLinks.map((link) => (
<Link key={link.href} href={link.href}>
<a>
<div
className={`px-4 py-2 text-sm font-medium rounded-md ${
(
link.label === "Import"
? router.asPath.includes(link.href)
: router.asPath === link.href
)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
}`}
>
{link.label}
</div>
</a>
</Link>
))}
</div>
</div>
);
};

View File

@ -30,8 +30,7 @@ const integrationDetails: { [key: string]: any } = {
}, },
slack: { slack: {
logo: SlackLogo, logo: SlackLogo,
description: description: "Get regular updates and control which notification you want to receive.",
"Connect your slack channel to this project to get regular updates. Control which notification you want to receive.",
}, },
}; };
@ -93,19 +92,19 @@ export const SingleIntegration: React.FC<Props> = ({ integration }) => {
return ( return (
<> <>
{integration && ( {integration && (
<div className="flex items-center justify-between gap-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5"> <div className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="h-12 w-12 flex-shrink-0"> <div className="h-10 w-10 flex-shrink-0">
<Image <Image
src={integrationDetails[integration.integration_detail.provider].logo} src={integrationDetails[integration.integration_detail.provider].logo}
alt={`${integration.integration_detail.title} Logo`} alt={`${integration.integration_detail.title} Logo`}
/> />
</div> </div>
<div> <div>
<h3 className="flex items-center gap-4 text-xl font-semibold"> <h3 className="flex items-center gap-4 text-sm font-medium">
{integration.integration_detail.title} {integration.integration_detail.title}
</h3> </h3>
<p className="text-sm text-custom-text-200"> <p className="text-sm text-custom-text-200 tracking-tight">
{integrationDetails[integration.integration_detail.provider].description} {integrationDetails[integration.integration_detail.provider].description}
</p> </p>
</div> </div>

View File

@ -9,13 +9,10 @@ import stateService from "services/state.service";
// ui // ui
import { Tooltip } from "components/ui"; import { Tooltip } from "components/ui";
// icons // icons
import { import { ArrowDownIcon, ArrowUpIcon } from "@heroicons/react/24/outline";
ArrowDownIcon,
ArrowUpIcon,
PencilSquareIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { StateGroupIcon } from "components/icons"; import { StateGroupIcon } from "components/icons";
import { Pencil, X } from "lucide-react";
// helpers // helpers
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
import { groupBy, orderArrayBy } from "helpers/array.helper"; import { groupBy, orderArrayBy } from "helpers/array.helper";
@ -160,15 +157,15 @@ export const SingleState: React.FC<Props> = ({
}; };
return ( return (
<div className="group flex items-center justify-between gap-2 border-custom-border-200 bg-custom-background-100 p-5 first:rounded-t-[10px] last:rounded-b-[10px]"> <div className="group flex items-center justify-between gap-2 rounded border border-custom-border-200 bg-custom-background-100 px-4 py-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<StateGroupIcon stateGroup={state.group} color={state.color} height="20px" width="20px" /> <StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
<div> <div>
<h6 className="text-sm">{addSpaceIfCamelCase(state.name)}</h6> <h6 className="text-sm font-medium">{addSpaceIfCamelCase(state.name)}</h6>
<p className="text-xs text-custom-text-200">{state.description}</p> <p className="text-xs text-custom-text-200">{state.description}</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="group flex items-center gap-2.5">
{index !== 0 && ( {index !== 0 && (
<button <button
type="button" type="button"
@ -192,20 +189,25 @@ export const SingleState: React.FC<Props> = ({
) : ( ) : (
<button <button
type="button" type="button"
className="hidden text-xs text-custom-text-200 group-hover:inline-block" className="hidden text-xs text-custom-sidebar-text-400 group-hover:inline-block"
onClick={handleMakeDefault} onClick={handleMakeDefault}
disabled={isSubmitting} disabled={isSubmitting}
> >
Set as default Mark as default
</button> </button>
)} )}
<div className=" items-center gap-2.5 hidden group-hover:flex">
<button type="button" className="grid place-items-center" onClick={handleEditState}>
<PencilSquareIcon className="h-4 w-4 text-custom-text-200" />
</button>
<button <button
type="button" type="button"
className={`${ className="grid place-items-center group-hover:opacity-100 opacity-0"
onClick={handleEditState}
>
<Pencil className="h-3.5 w-3.5 text-custom-text-200" />
</button>
<button
type="button"
className={`group-hover:opacity-100 opacity-0 ${
state.default || groupLength === 1 ? "cursor-not-allowed" : "" state.default || groupLength === 1 ? "cursor-not-allowed" : ""
} grid place-items-center`} } grid place-items-center`}
onClick={handleDeleteState} onClick={handleDeleteState}
@ -213,17 +215,18 @@ export const SingleState: React.FC<Props> = ({
> >
{state.default ? ( {state.default ? (
<Tooltip tooltipContent="Cannot delete the default state."> <Tooltip tooltipContent="Cannot delete the default state.">
<TrashIcon className="h-4 w-4 text-red-500" /> <X className="h-3.5 w-3.5 text-red-500" />
</Tooltip> </Tooltip>
) : groupLength === 1 ? ( ) : groupLength === 1 ? (
<Tooltip tooltipContent="Cannot have an empty group."> <Tooltip tooltipContent="Cannot have an empty group.">
<TrashIcon className="h-4 w-4 text-red-500" /> <X className="h-3.5 w-3.5 text-red-500" />
</Tooltip> </Tooltip>
) : ( ) : (
<TrashIcon className="h-4 w-4 text-red-500" /> <X className="h-3.5 w-3.5 text-red-500" />
)} )}
</button> </button>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -6,8 +6,8 @@ type Props = {
}; };
export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => ( export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => (
<div className="flex flex-col items-start gap-3"> <div className="flex flex-col items-start gap-3 py-3.5 border-b border-custom-border-200">
<h3 className="text-2xl font-semibold">{bannerName}</h3> <h3 className="text-xl font-medium">{bannerName}</h3>
{description && ( {description && (
<div className="flex items-center gap-3 rounded-[10px] border border-custom-primary/75 bg-custom-primary/5 p-4 text-sm text-custom-text-100"> <div className="flex items-center gap-3 rounded-[10px] border border-custom-primary/75 bg-custom-primary/5 p-4 text-sm text-custom-text-100">
<ExclamationIcon height={24} width={24} className="fill-current text-custom-text-100" /> <ExclamationIcon height={24} width={24} className="fill-current text-custom-text-100" />

View File

@ -18,24 +18,24 @@ export const ToggleSwitch: React.FC<Props> = (props) => {
disabled={disabled} disabled={disabled}
onChange={onChange} onChange={onChange}
className={`relative flex-shrink-0 inline-flex ${ className={`relative flex-shrink-0 inline-flex ${
size === "sm" ? "h-3.5 w-6" : size === "md" ? "h-4 w-7" : "h-6 w-11" size === "sm" ? "h-4 w-6" : size === "md" ? "h-5 w-8" : "h-6 w-10"
} flex-shrink-0 cursor-pointer rounded-full border-2 border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${ } flex-shrink-0 cursor-pointer rounded-full border border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
value ? "bg-green-500" : "bg-custom-background-80" value ? "bg-custom-primary-100" : "bg-gray-700"
} ${className || ""}`} } ${className || ""}`}
> >
<span className="sr-only">{label}</span> <span className="sr-only">{label}</span>
<span <span
aria-hidden="true" aria-hidden="true"
className={`inline-block ${ className={`self-center inline-block ${
size === "sm" ? "h-2.5 w-2.5" : size === "md" ? "h-3 w-3" : "h-5 w-5" size === "sm" ? "h-2 w-2" : size === "md" ? "h-3 w-3" : "h-4 w-4"
} transform rounded-full shadow ring-0 transition duration-200 ease-in-out ${ } transform rounded-full shadow ring-0 transition duration-200 ease-in-out ${
value value
? (size === "sm" ? (size === "sm"
? "translate-x-2.5"
: size === "md"
? "translate-x-3" ? "translate-x-3"
: size === "md"
? "translate-x-4"
: "translate-x-5") + " bg-white" : "translate-x-5") + " bg-white"
: "translate-x-0 bg-custom-background-90" : "translate-x-1 bg-custom-background-90"
}`} }`}
/> />
</Switch> </Switch>

View File

@ -57,7 +57,7 @@
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lowlight": "^2.9.0", "lowlight": "^2.9.0",
"lucide-react": "^0.263.1", "lucide-react": "^0.269.0",
"mobx": "^6.10.0", "mobx": "^6.10.0",
"mobx-react-lite": "^4.0.3", "mobx-react-lite": "^4.0.3",
"next": "12.3.2", "next": "12.3.2",

View File

@ -13,8 +13,8 @@ import useUserAuth from "hooks/use-user-auth";
import useProjectDetails from "hooks/use-project-details"; import useProjectDetails from "hooks/use-project-details";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { SettingsHeader } from "components/project";
import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation"; import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation";
import { SettingsSidebar } from "components/project";
// ui // ui
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types // types
@ -75,11 +75,16 @@ const AutomationsSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8"> <div className="flex flex-row gap-2">
<SettingsHeader /> <div className="w-80 py-8">
<section className="space-y-5"> <SettingsSidebar />
<AutoCloseAutomation projectDetails={projectDetails} handleChange={handleChange} /> </div>
<section className="pr-9 py-8 w-full">
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Automations</h3>
</div>
<AutoArchiveAutomation projectDetails={projectDetails} handleChange={handleChange} /> <AutoArchiveAutomation projectDetails={projectDetails} handleChange={handleChange} />
<AutoCloseAutomation projectDetails={projectDetails} handleChange={handleChange} />
</section> </section>
</div> </div>
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>

View File

@ -1,241 +0,0 @@
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// services
import projectService from "services/project.service";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
// components
import { SettingsHeader } from "components/project";
// ui
import { CustomSelect, Loader, SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// types
import { IProject, IUserLite, IWorkspace } from "types";
import type { NextPage } from "next";
// fetch-keys
import { PROJECTS_LIST, PROJECT_DETAILS, PROJECT_MEMBERS } from "constants/fetch-keys";
// helper
import { truncateText } from "helpers/string.helper";
const defaultValues: Partial<IProject> = {
project_lead: null,
default_assignee: null,
};
const ControlSettings: NextPage = () => {
const { setToastAlert } = useToast();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { user } = useUserAuth();
const { data: projectDetails } = useSWR<IProject>(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
);
const { data: people } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const {
handleSubmit,
reset,
control,
formState: { isSubmitting },
} = useForm<IProject>({ defaultValues });
const onSubmit = async (formData: IProject) => {
if (!workspaceSlug || !projectId || !projectDetails) return;
const payload: Partial<IProject> = {
default_assignee: formData.default_assignee,
project_lead: formData.project_lead,
};
await projectService
.updateProject(workspaceSlug as string, projectId as string, payload, user)
.then((res) => {
mutate(PROJECT_DETAILS(projectId as string));
mutate(
PROJECTS_LIST(workspaceSlug as string, {
is_favorite: "all",
})
);
setToastAlert({
title: "Success",
type: "success",
message: "Project updated successfully",
});
})
.catch((err) => {
console.log(err);
});
};
useEffect(() => {
if (projectDetails)
reset({
...projectDetails,
default_assignee: projectDetails.default_assignee?.id ?? projectDetails.default_assignee,
project_lead: (projectDetails.project_lead as IUserLite)?.id ?? projectDetails.project_lead,
workspace: (projectDetails.workspace as IWorkspace).id,
});
}, [projectDetails, reset]);
return (
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 32)}`}
link={`/${workspaceSlug}/projects/${projectId}/issues`}
linkTruncate
/>
<BreadcrumbItem title="Control Settings" unshrinkTitle />
</Breadcrumbs>
}
>
<form onSubmit={handleSubmit(onSubmit)} className="p-8">
<SettingsHeader />
<div className="space-y-8 sm:space-y-12">
<div className="grid grid-cols-12 items-start gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Project Lead</h4>
<p className="text-sm text-custom-text-200">Select the project leader.</p>
</div>
<div className="col-span-12 sm:col-span-6">
{projectDetails ? (
<Controller
name="project_lead"
control={control}
render={({ field }) => (
<CustomSelect
{...field}
label={
people?.find((person) => person.member.id === field.value)?.member
.display_name ?? <span className="text-custom-text-200">Select lead</span>
}
width="w-full"
input
>
{people?.map((person) => (
<CustomSelect.Option
key={person.member.id}
value={person.member.id}
className="flex items-center gap-2"
>
<div className="flex items-center gap-2">
{person.member.avatar && person.member.avatar !== "" ? (
<div className="relative h-4 w-4">
<img
src={person.member.avatar}
className="absolute top-0 left-0 h-full w-full object-cover rounded-full"
alt="User Avatar"
/>
</div>
) : (
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
{person.member.display_name?.charAt(0)}
</div>
)}
{person.member.display_name}
</div>
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
) : (
<Loader className="h-9 w-full">
<Loader.Item width="100%" height="100%" />
</Loader>
)}
</div>
</div>
<div className="grid grid-cols-12 items-start gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Default Assignee</h4>
<p className="text-sm text-custom-text-200">
Select the default assignee for the project.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
{projectDetails ? (
<Controller
name="default_assignee"
control={control}
render={({ field }) => (
<CustomSelect
{...field}
label={
people?.find((p) => p.member.id === field.value)?.member.display_name ?? (
<span className="text-custom-text-200">Select default assignee</span>
)
}
width="w-full"
input
>
{people?.map((person) => (
<CustomSelect.Option
key={person.member.id}
value={person.member.id}
className="flex items-center gap-2"
>
<div className="flex items-center gap-2">
{person.member.avatar && person.member.avatar !== "" ? (
<div className="relative h-4 w-4">
<img
src={person.member.avatar}
className="absolute top-0 left-0 h-full w-full object-cover rounded-full"
alt="User Avatar"
/>
</div>
) : (
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
{person.member.display_name?.charAt(0)}
</div>
)}
{person.member.display_name}
</div>
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
) : (
<Loader className="h-9 w-full">
<Loader.Item width="100%" height="100%" />
</Loader>
)}
</div>
</div>
<div className="sm:text-right">
<SecondaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Updating Project..." : "Update Project"}
</SecondaryButton>
</div>
</div>
</form>
</ProjectAuthorizationWrapper>
);
};
export default ControlSettings;

View File

@ -13,12 +13,12 @@ import useProjectDetails from "hooks/use-project-details";
import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// components // components
import { CreateUpdateEstimateModal, SingleEstimate } from "components/estimates"; import { CreateUpdateEstimateModal, SingleEstimate } from "components/estimates";
import { SettingsHeader } from "components/project"; import { SettingsSidebar } from "components/project";
//hooks //hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
// ui // ui
import { EmptyState, Loader, SecondaryButton } from "components/ui"; import { EmptyState, Loader, PrimaryButton, SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
@ -125,22 +125,23 @@ const EstimatesSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="h-full flex flex-col p-8 overflow-hidden"> <div className="flex flex-row gap-2 h-full overflow-hidden">
<SettingsHeader /> <div className="w-80 py-8">
<section className="flex items-center justify-between"> <SettingsSidebar />
<h3 className="text-2xl font-semibold">Estimates</h3> </div>
<div className="pr-9 py-8 flex flex-col w-full">
<section className="flex items-center justify-between pt-2 pb-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Estimates</h3>
<div className="col-span-12 space-y-5 sm:col-span-7"> <div className="col-span-12 space-y-5 sm:col-span-7">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <PrimaryButton
className="flex cursor-pointer items-center gap-2 text-custom-primary-100 hover:text-custom-primary-200"
onClick={() => { onClick={() => {
setEstimateToUpdate(undefined); setEstimateToUpdate(undefined);
setEstimateFormOpen(true); setEstimateFormOpen(true);
}} }}
> >
<PlusIcon className="h-4 w-4" /> Add Estimate
Create New Estimate </PrimaryButton>
</div>
{projectDetails?.estimate && ( {projectDetails?.estimate && (
<SecondaryButton onClick={disableEstimates}>Disable Estimates</SecondaryButton> <SecondaryButton onClick={disableEstimates}>Disable Estimates</SecondaryButton>
)} )}
@ -149,7 +150,7 @@ const EstimatesSettings: NextPage = () => {
</section> </section>
{estimatesList ? ( {estimatesList ? (
estimatesList.length > 0 ? ( estimatesList.length > 0 ? (
<section className="h-full mt-5 divide-y divide-custom-border-200 rounded-xl border border-custom-border-200 bg-custom-background-100 px-6 overflow-y-auto"> <section className="h-full bg-custom-background-100 overflow-y-auto">
{estimatesList.map((estimate) => ( {estimatesList.map((estimate) => (
<SingleEstimate <SingleEstimate
key={estimate.id} key={estimate.id}
@ -186,6 +187,7 @@ const EstimatesSettings: NextPage = () => {
</Loader> </Loader>
)} )}
</div> </div>
</div>
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>
</> </>
); );

View File

@ -13,13 +13,13 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
// components // components
import { SettingsHeader } from "components/project"; import { SettingsSidebar } from "components/project";
// ui // ui
import { SecondaryButton, ToggleSwitch } from "components/ui"; import { ToggleSwitch } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { ContrastIcon, PeopleGroupIcon, ViewListIcon, InboxIcon } from "components/icons"; import { ModuleIcon } from "components/icons";
import { DocumentTextIcon } from "@heroicons/react/24/outline"; import { Contrast, FileText, Inbox, Layers } from "lucide-react";
// types // types
import { IProject } from "types"; import { IProject } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
@ -33,35 +33,35 @@ const featuresList = [
title: "Cycles", title: "Cycles",
description: description:
"Cycles are enabled for all the projects in this workspace. Access them from the sidebar.", "Cycles are enabled for all the projects in this workspace. Access them from the sidebar.",
icon: <ContrastIcon color="#3f76ff" width={28} height={28} className="flex-shrink-0" />, icon: <Contrast className="h-4 w-4 text-custom-primary-100 flex-shrink-0" />,
property: "cycle_view", property: "cycle_view",
}, },
{ {
title: "Modules", title: "Modules",
description: description:
"Modules are enabled for all the projects in this workspace. Access it from the sidebar.", "Modules are enabled for all the projects in this workspace. Access it from the sidebar.",
icon: <PeopleGroupIcon color="#ff6b00" width={28} height={28} className="flex-shrink-0" />, icon: <ModuleIcon width={16} height={16} className="flex-shrink-0" />,
property: "module_view", property: "module_view",
}, },
{ {
title: "Views", title: "Views",
description: description:
"Views are enabled for all the projects in this workspace. Access it from the sidebar.", "Views are enabled for all the projects in this workspace. Access it from the sidebar.",
icon: <ViewListIcon color="#05c3ff" width={28} height={28} className="flex-shrink-0" />, icon: <Layers className="h-4 w-4 text-cyan-500 flex-shrink-0" />,
property: "issue_views_view", property: "issue_views_view",
}, },
{ {
title: "Pages", title: "Pages",
description: description:
"Pages are enabled for all the projects in this workspace. Access it from the sidebar.", "Pages are enabled for all the projects in this workspace. Access it from the sidebar.",
icon: <DocumentTextIcon color="#fcbe1d" width={28} height={28} className="flex-shrink-0" />, icon: <FileText className="h-4 w-4 text-red-400 flex-shrink-0" />,
property: "page_view", property: "page_view",
}, },
{ {
title: "Inbox", title: "Inbox",
description: description:
"Inbox are enabled for all the projects in this workspace. Access it from the issues views page.", "Inbox are enabled for all the projects in this workspace. Access it from the issues views page.",
icon: <InboxIcon color="#fcbe1d" width={24} height={24} className="flex-shrink-0" />, icon: <Inbox className="h-4 w-4 text-cyan-500 flex-shrink-0" />,
property: "inbox_view", property: "inbox_view",
}, },
]; ];
@ -149,21 +149,29 @@ const FeaturesSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8"> <div className="flex flex-row gap-2">
<SettingsHeader /> <div className="w-80 py-8">
<section className="space-y-5"> <SettingsSidebar />
<h3 className="text-2xl font-semibold">Features</h3> </div>
<div className="space-y-5"> <section className="pr-9 py-8 w-full">
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Features</h3>
</div>
<div>
{featuresList.map((feature) => ( {featuresList.map((feature) => (
<div <div
key={feature.property} key={feature.property}
className="flex items-center justify-between gap-x-8 gap-y-2 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-5" className="flex items-center justify-between gap-x-8 gap-y-2 border-b border-custom-border-200 bg-custom-background-100 p-4"
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
{feature.icon} {feature.icon}
</div>
<div className=""> <div className="">
<h4 className="text-lg font-semibold">{feature.title}</h4> <h4 className="text-sm font-medium">{feature.title}</h4>
<p className="text-sm text-custom-text-200">{feature.description}</p> <p className="text-sm text-custom-text-200 tracking-tight">
{feature.description}
</p>
</div> </div>
</div> </div>
<ToggleSwitch <ToggleSwitch
@ -187,29 +195,11 @@ const FeaturesSettings: NextPage = () => {
[feature.property]: !projectDetails?.[feature.property as keyof IProject], [feature.property]: !projectDetails?.[feature.property as keyof IProject],
}); });
}} }}
size="lg" size="sm"
/> />
</div> </div>
))} ))}
</div> </div>
<div className="flex items-center gap-2 text-custom-text-200">
<a
href="https://plane.so/"
target="_blank"
rel="noreferrer"
className="hover:text-custom-text-100"
>
<SecondaryButton outline>Plane is open-source, view Roadmap</SecondaryButton>
</a>
<a
href="https://github.com/makeplane/plane"
target="_blank"
rel="noreferrer"
className="hover:text-custom-text-100"
>
<SecondaryButton outline>Star us on GitHub</SecondaryButton>
</a>
</div>
</section> </section>
</div> </div>
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>

View File

@ -4,6 +4,8 @@ import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
// headless ui
import { Disclosure, Transition } from "@headlessui/react";
// react-hook-form // react-hook-form
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// layouts // layouts
@ -11,7 +13,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// services // services
import projectService from "services/project.service"; import projectService from "services/project.service";
// components // components
import { DeleteProjectModal, SettingsHeader } from "components/project"; import { DeleteProjectModal, SettingsSidebar } from "components/project";
import { ImagePickerPopover } from "components/core"; import { ImagePickerPopover } from "components/core";
import EmojiIconPicker from "components/emoji-icon-picker"; import EmojiIconPicker from "components/emoji-icon-picker";
// hooks // hooks
@ -25,11 +27,14 @@ import {
CustomSelect, CustomSelect,
SecondaryButton, SecondaryButton,
DangerButton, DangerButton,
Icon,
PrimaryButton,
} from "components/ui"; } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// helpers // helpers
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types // types
import { IProject, IWorkspace } from "types"; import { IProject, IWorkspace } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
@ -185,17 +190,21 @@ const GeneralSettings: NextPage = () => {
onClose={() => setSelectedProject(null)} onClose={() => setSelectedProject(null)}
user={user} user={user}
/> />
<form onSubmit={handleSubmit(onSubmit)} className="p-8"> <form onSubmit={handleSubmit(onSubmit)}>
<SettingsHeader /> <div className="flex flex-row gap-2">
<div className={`space-y-8 sm:space-y-12 ${isAdmin ? "" : "opacity-60"}`}> <div className="w-80 py-8">
<div className="grid grid-cols-12 items-start gap-4 sm:gap-16"> <SettingsSidebar />
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Icon & Name</h4>
<p className="text-sm text-custom-text-200">
Select an icon and a name for your project.
</p>
</div> </div>
<div className="col-span-12 flex gap-2 sm:col-span-6"> <div className={`pr-9 py-8 w-full ${isAdmin ? "" : "opacity-60"}`}>
<div className="relative h-44 w-full mt-6">
<img
src={watch("cover_image")!}
alt={watch("cover_image")!}
className="h-44 w-full rounded-md object-cover"
/>
<div className="flex items-end justify-between absolute bottom-4 w-full px-4">
<div className="flex gap-3">
<div className="flex items-center justify-center bg-custom-background-90 h-[52px] w-[52px] rounded-lg">
{projectDetails ? ( {projectDetails ? (
<div className="h-7 w-7 grid place-items-center"> <div className="h-7 w-7 grid place-items-center">
<Controller <Controller
@ -216,66 +225,24 @@ const GeneralSettings: NextPage = () => {
<Loader.Item height="46px" width="46px" /> <Loader.Item height="46px" width="46px" />
</Loader> </Loader>
)} )}
</div>
<div className="flex flex-col gap-1 text-white">
<span className="text-lg font-semibold">{watch("name")}</span>
<span className="flex items-center gap-2 text-sm">
<span>
{watch("identifier")} . {currentNetwork?.label}
</span>
</span>
</div>
</div>
<div className="flex justify-center">
{projectDetails ? ( {projectDetails ? (
<Input <div>
id="name" <Controller
name="name" control={control}
error={errors.name} name="cover_image"
register={register} render={({ field: { value, onChange } }) => (
placeholder="Project Name"
validations={{
required: "Name is required",
}}
disabled={!isAdmin}
/>
) : (
<Loader>
<Loader.Item height="46px" width="225px" />
</Loader>
)}
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Description</h4>
<p className="text-sm text-custom-text-200">Give a description to your project.</p>
</div>
<div className="col-span-12 sm:col-span-6">
{projectDetails ? (
<TextArea
id="description"
name="description"
error={errors.description}
register={register}
placeholder="Enter project description"
validations={{}}
className="min-h-[46px] text-sm"
disabled={!isAdmin}
/>
) : (
<Loader className="w-full">
<Loader.Item height="46px" width="full" />
</Loader>
)}
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Cover Photo</h4>
<p className="text-sm text-custom-text-200">
Select your cover photo from the given library.
</p>
</div>
<div className="col-span-12 sm:col-span-6">
{watch("cover_image") ? (
<div className="h-32 w-full rounded border border-custom-border-200 p-1">
<div className="relative h-full w-full rounded">
<img
src={watch("cover_image")!}
className="absolute top-0 left-0 h-full w-full object-cover rounded"
alt={projectDetails?.name ?? "Cover image"}
/>
<div className="absolute bottom-0 flex w-full justify-end">
<ImagePickerPopover <ImagePickerPopover
label={"Change cover"} label={"Change cover"}
onChange={(imageUrl) => { onChange={(imageUrl) => {
@ -284,24 +251,64 @@ const GeneralSettings: NextPage = () => {
value={watch("cover_image")} value={watch("cover_image")}
disabled={!isAdmin} disabled={!isAdmin}
/> />
</div> )}
</div> />
</div> </div>
) : ( ) : (
<Loader className="w-full"> <Loader>
<Loader.Item height="46px" width="full" /> <Loader.Item height="32px" width="108px" />
</Loader> </Loader>
)} )}
</div> </div>
</div> </div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Identifier</h4>
<p className="text-sm text-custom-text-200">
Create a 1-6 characters{"'"} identifier for the project.
</p>
</div> </div>
<div className="col-span-12 sm:col-span-6">
<div className="flex flex-col gap-8 my-8">
<div className="flex flex-col gap-1">
<h4 className="text-sm">Project Name</h4>
{projectDetails ? (
<Input
id="name"
name="name"
error={errors.name}
register={register}
className="!p-3 rounded-md font-medium"
placeholder="Project Name"
validations={{
required: "Name is required",
}}
disabled={!isAdmin}
/>
) : (
<Loader>
<Loader.Item height="46px" width="100%" />
</Loader>
)}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Description</h4>
{projectDetails ? (
<TextArea
id="description"
name="description"
error={errors.description}
register={register}
placeholder="Enter project description"
validations={{}}
className="min-h-[102px] text-sm"
disabled={!isAdmin}
/>
) : (
<Loader className="w-full">
<Loader.Item height="102px" width="full" />
</Loader>
)}
</div>
<div className="flex items-center justify-between gap-10 w-full">
<div className="flex flex-col gap-1 w-1/2">
<h4 className="text-sm">Identifier</h4>
{projectDetails ? ( {projectDetails ? (
<Input <Input
id="identifier" id="identifier"
@ -313,7 +320,8 @@ const GeneralSettings: NextPage = () => {
validations={{ validations={{
required: "Identifier is required", required: "Identifier is required",
validate: (value) => validate: (value) =>
/^[A-Z0-9]+$/.test(value.toUpperCase()) || "Identifier must be in uppercase.", /^[A-Z0-9]+$/.test(value.toUpperCase()) ||
"Identifier must be in uppercase.",
minLength: { minLength: {
value: 1, value: 1,
message: "Identifier must at least be of 1 character", message: "Identifier must at least be of 1 character",
@ -327,17 +335,13 @@ const GeneralSettings: NextPage = () => {
/> />
) : ( ) : (
<Loader> <Loader>
<Loader.Item height="46px" width="160px" /> <Loader.Item height="36px" width="100%" />
</Loader> </Loader>
)} )}
</div> </div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16"> <div className="flex flex-col gap-1 w-1/2">
<div className="col-span-12 sm:col-span-6"> <h4 className="text-sm">Network</h4>
<h4 className="text-lg font-semibold">Network</h4>
<p className="text-sm text-custom-text-200">Select privacy type for the project.</p>
</div>
<div className="col-span-12 sm:col-span-6">
{projectDetails ? ( {projectDetails ? (
<Controller <Controller
name="network" name="network"
@ -347,6 +351,7 @@ const GeneralSettings: NextPage = () => {
value={value} value={value}
onChange={onChange} onChange={onChange}
label={currentNetwork?.label ?? "Select network"} label={currentNetwork?.label ?? "Select network"}
className="!border-custom-border-200 !shadow-none"
input input
disabled={!isAdmin} disabled={!isAdmin}
> >
@ -360,54 +365,83 @@ const GeneralSettings: NextPage = () => {
/> />
) : ( ) : (
<Loader className="w-full"> <Loader className="w-full">
<Loader.Item height="46px" width="160px" /> <Loader.Item height="46px" width="100%" />
</Loader> </Loader>
)} )}
</div> </div>
</div> </div>
{isAdmin && ( <div className="flex items-center justify-between py-2">
<>
<div className="sm:text-right">
{projectDetails ? ( {projectDetails ? (
<SecondaryButton type="submit" loading={isSubmitting} disabled={!isAdmin}> <>
<PrimaryButton type="submit" loading={isSubmitting} disabled={!isAdmin}>
{isSubmitting ? "Updating Project..." : "Update Project"} {isSubmitting ? "Updating Project..." : "Update Project"}
</SecondaryButton> </PrimaryButton>
<span className="text-sm text-custom-sidebar-text-400 italic">
Created on {renderShortDateWithYearFormat(projectDetails?.created_at)}
</span>
</>
) : ( ) : (
<Loader className="mt-2 w-full"> <Loader className="mt-2 w-full">
<Loader.Item height="34px" width="100px" /> <Loader.Item height="34px" width="100px" />
</Loader> </Loader>
)} )}
</div> </div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold">Danger Zone</h4>
<p className="text-sm text-custom-text-200">
The danger zone of the project delete page is a critical area that requires
careful consideration and attention. When deleting a project, all of the data
and resources within that project will be permanently removed and cannot be
recovered.
</p>
</div> </div>
<div className="col-span-12 sm:col-span-6">
<Disclosure as="div" className="border-t border-custom-border-400">
{({ open }) => (
<div className="w-full">
<Disclosure.Button
as="button"
type="button"
className="flex items-center justify-between w-full py-4"
>
<span className="text-xl tracking-tight">Danger Zone</span>
<Icon iconName={open ? "expand_more" : "expand_less"} className="!text-2xl" />
</Disclosure.Button>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="flex flex-col gap-8">
<span className="text-sm tracking-tight">
The danger zone of the project delete page is a critical area that
requires careful consideration and attention. When deleting a project, all
of the data and resources within that project will be permanently removed
and cannot be recovered.
</span>
<div>
{projectDetails ? ( {projectDetails ? (
<div> <div>
<DangerButton <DangerButton
onClick={() => setSelectedProject(projectDetails.id ?? null)} onClick={() => setSelectedProject(projectDetails.id ?? null)}
className="!text-sm"
outline outline
> >
Delete Project Delete my project
</DangerButton> </DangerButton>
</div> </div>
) : ( ) : (
<Loader className="mt-2 w-full"> <Loader className="mt-2 w-full">
<Loader.Item height="46px" width="100px" /> <Loader.Item height="38px" width="144px" />
</Loader> </Loader>
)} )}
</div> </div>
</div> </div>
</> </Disclosure.Panel>
</Transition>
</div>
)} )}
</Disclosure>
</div>
</div> </div>
</form> </form>
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>

View File

@ -10,7 +10,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import IntegrationService from "services/integration"; import IntegrationService from "services/integration";
import projectService from "services/project.service"; import projectService from "services/project.service";
// components // components
import { SettingsHeader, SingleIntegration } from "components/project"; import { SettingsSidebar, SingleIntegration } from "components/project";
// ui // ui
import { EmptyState, IntegrationAndImportExportBanner, Loader } from "components/ui"; import { EmptyState, IntegrationAndImportExportBanner, Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -58,13 +58,15 @@ const ProjectIntegrations: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="h-full flex flex-col p-8 overflow-hidden"> <div className="flex flex-row gap-2 h-full overflow-hidden">
<SettingsHeader /> <div className="w-80 py-8">
<SettingsSidebar />
</div>
{workspaceIntegrations ? ( {workspaceIntegrations ? (
workspaceIntegrations.length > 0 ? ( workspaceIntegrations.length > 0 ? (
<section className="space-y-8 overflow-y-auto"> <section className="pr-9 py-8 overflow-y-auto w-full">
<IntegrationAndImportExportBanner bannerName="Integrations" /> <IntegrationAndImportExportBanner bannerName="Integrations" />
<div className="space-y-5"> <div>
{workspaceIntegrations.map((integration) => ( {workspaceIntegrations.map((integration) => (
<SingleIntegration <SingleIntegration
key={integration.integration_detail.id} key={integration.integration_detail.id}

View File

@ -19,7 +19,7 @@ import {
SingleLabel, SingleLabel,
SingleLabelGroup, SingleLabelGroup,
} from "components/labels"; } from "components/labels";
import { SettingsHeader } from "components/project"; import { SettingsSidebar } from "components/project";
// ui // ui
import { EmptyState, Loader, PrimaryButton } from "components/ui"; import { EmptyState, Loader, PrimaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -113,20 +113,23 @@ const LabelsSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8"> <div className="flex flex-row gap-2">
<SettingsHeader /> <div className="w-80 py-8">
<section className="grid grid-cols-12 gap-10"> <SettingsSidebar />
<div className="col-span-12 sm:col-span-5"> </div>
<h3 className="text-2xl font-semibold">Labels</h3> <section className="pr-9 py-8 gap-10 w-full">
<p className="text-custom-text-200">Manage the labels of this project.</p> <div className="flex items-center justify-between pt-2 pb-3.5 border-b border-custom-border-200">
<PrimaryButton onClick={newLabel} size="sm" className="mt-4"> <h3 className="text-xl font-medium">Labels</h3>
<span className="flex items-center gap-2">
<PlusIcon className="h-4 w-4" /> <PrimaryButton
New label onClick={newLabel}
</span> size="sm"
className="flex items-center justify-center"
>
Add label
</PrimaryButton> </PrimaryButton>
</div> </div>
<div className="col-span-12 space-y-5 sm:col-span-7"> <div className="space-y-3 py-6">
{labelForm && ( {labelForm && (
<CreateUpdateLabelInline <CreateUpdateLabelInline
labelForm={labelForm} labelForm={labelForm}

View File

@ -1,9 +1,9 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
import useSWR from "swr"; import useSWR, { mutate } from "swr";
// services // services
import projectService from "services/project.service"; import projectService from "services/project.service";
@ -13,22 +13,35 @@ import useToast from "hooks/use-toast";
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
import useProjectMembers from "hooks/use-project-members"; import useProjectMembers from "hooks/use-project-members";
import useProjectDetails from "hooks/use-project-details"; import useProjectDetails from "hooks/use-project-details";
import { Controller, useForm } from "react-hook-form";
// layouts // layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// components // components
import ConfirmProjectMemberRemove from "components/project/confirm-project-member-remove"; import ConfirmProjectMemberRemove from "components/project/confirm-project-member-remove";
import SendProjectInvitationModal from "components/project/send-project-invitation-modal"; import SendProjectInvitationModal from "components/project/send-project-invitation-modal";
import { SettingsHeader } from "components/project"; import { MemberSelect, SettingsSidebar } from "components/project";
// ui // ui
import { CustomMenu, CustomSelect, Loader } from "components/ui"; import {
CustomMenu,
CustomSearchSelect,
CustomSelect,
Icon,
Loader,
PrimaryButton,
SecondaryButton,
} from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons // icons
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
import { IProject, IUserLite, IWorkspace } from "types";
// fetch-keys // fetch-keys
import { import {
PROJECTS_LIST,
PROJECT_DETAILS,
PROJECT_INVITATIONS_WITH_EMAIL, PROJECT_INVITATIONS_WITH_EMAIL,
PROJECT_MEMBERS,
PROJECT_MEMBERS_WITH_EMAIL, PROJECT_MEMBERS_WITH_EMAIL,
WORKSPACE_DETAILS, WORKSPACE_DETAILS,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
@ -37,6 +50,11 @@ import { ROLE } from "constants/workspace";
// helper // helper
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
const defaultValues: Partial<IProject> = {
project_lead: null,
default_assignee: null,
};
const MembersSettings: NextPage = () => { const MembersSettings: NextPage = () => {
const [inviteModal, setInviteModal] = useState(false); const [inviteModal, setInviteModal] = useState(false);
const [selectedRemoveMember, setSelectedRemoveMember] = useState<string | null>(null); const [selectedRemoveMember, setSelectedRemoveMember] = useState<string | null>(null);
@ -55,11 +73,25 @@ const MembersSettings: NextPage = () => {
Boolean(workspaceSlug && projectId) Boolean(workspaceSlug && projectId)
); );
const {
handleSubmit,
reset,
control,
formState: { isSubmitting },
} = useForm<IProject>({ defaultValues });
const { data: activeWorkspace } = useSWR( const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null) () => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
); );
const { data: people } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const { data: projectMembers, mutate: mutateMembers } = useSWR( const { data: projectMembers, mutate: mutateMembers } = useSWR(
workspaceSlug && projectId workspaceSlug && projectId
? PROJECT_MEMBERS_WITH_EMAIL(workspaceSlug.toString(), projectId.toString()) ? PROJECT_MEMBERS_WITH_EMAIL(workspaceSlug.toString(), projectId.toString())
@ -110,6 +142,76 @@ const MembersSettings: NextPage = () => {
const handleProjectInvitationSuccess = () => {}; const handleProjectInvitationSuccess = () => {};
const onSubmit = async (formData: IProject) => {
if (!workspaceSlug || !projectId || !projectDetails) return;
const payload: Partial<IProject> = {
default_assignee: formData.default_assignee,
project_lead: formData.project_lead === "none" ? null : formData.project_lead,
};
await projectService
.updateProject(workspaceSlug as string, projectId as string, payload, user)
.then((res) => {
mutate(PROJECT_DETAILS(projectId as string));
mutate(
PROJECTS_LIST(workspaceSlug as string, {
is_favorite: "all",
})
);
setToastAlert({
title: "Success",
type: "success",
message: "Project updated successfully",
});
})
.catch((err) => {
console.log(err);
});
};
useEffect(() => {
if (projectDetails)
reset({
...projectDetails,
default_assignee: projectDetails.default_assignee?.id ?? projectDetails.default_assignee,
project_lead: (projectDetails.project_lead as IUserLite)?.id ?? projectDetails.project_lead,
workspace: (projectDetails.workspace as IWorkspace).id,
});
}, [projectDetails, reset]);
const submitChanges = async (formData: Partial<IProject>) => {
if (!workspaceSlug || !projectId) return;
const payload: Partial<IProject> = {
default_assignee: formData.default_assignee === "none" ? null : formData.default_assignee,
project_lead: formData.project_lead === "none" ? null : formData.project_lead,
};
await projectService
.updateProject(workspaceSlug as string, projectId as string, payload, user)
.then((res) => {
mutate(PROJECT_DETAILS(projectId as string));
mutate(
PROJECTS_LIST(workspaceSlug as string, {
is_favorite: "all",
})
);
setToastAlert({
title: "Success",
type: "success",
message: "Project updated successfully",
});
})
.catch((err) => {
console.log(err);
});
};
return ( return (
<ProjectAuthorizationWrapper <ProjectAuthorizationWrapper
breadcrumbs={ breadcrumbs={
@ -171,19 +273,69 @@ const MembersSettings: NextPage = () => {
user={user} user={user}
onSuccess={() => mutateMembers()} onSuccess={() => mutateMembers()}
/> />
<div className="p-8"> <div className="flex flex-row gap-2">
<SettingsHeader /> <div className="w-80 py-8">
<section className="space-y-5"> <SettingsSidebar />
<div className="flex items-end justify-between gap-4"> </div>
<h3 className="text-2xl font-semibold">Members</h3> <section className="pr-9 py-8 w-full">
<button <div className="flex items-center py-3.5 border-b border-custom-border-200">
type="button" <h3 className="text-xl font-medium">Defaults</h3>
className="flex items-center gap-2 text-custom-primary outline-none" </div>
onClick={() => setInviteModal(true)} <div className="flex flex-col gap-2 pb-4 w-full">
> <div className="flex items-center py-8 gap-4 w-full">
<PlusIcon className="h-4 w-4" /> <div className="flex flex-col gap-2 w-1/2">
Add Member <h4 className="text-sm">Project Lead</h4>
</button> <div className="">
{projectDetails ? (
<Controller
control={control}
name="project_lead"
render={({ field: { value } }) => (
<MemberSelect
value={value}
onChange={(val: string) => {
submitChanges({ project_lead: val });
}}
/>
)}
/>
) : (
<Loader className="h-9 w-full">
<Loader.Item width="100%" height="100%" />
</Loader>
)}
</div>
</div>
<div className="flex flex-col gap-2 w-1/2">
<h4 className="text-sm">Default Assignee</h4>
<div className="">
{projectDetails ? (
<Controller
control={control}
name="default_assignee"
render={({ field: { value } }) => (
<MemberSelect
value={value}
onChange={(val: string) => {
submitChanges({ default_assignee: val });
}}
/>
)}
/>
) : (
<Loader className="h-9 w-full">
<Loader.Item width="100%" height="100%" />
</Loader>
)}
</div>
</div>
</div>
</div>
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-200">
<h4 className="text-xl font-medium border-b border-custom-border-100">Members</h4>
<PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton>
</div> </div>
{!projectMembers || !projectInvitations ? ( {!projectMembers || !projectInvitations ? (
<Loader className="space-y-5"> <Loader className="space-y-5">
@ -193,10 +345,13 @@ const MembersSettings: NextPage = () => {
<Loader.Item height="40px" /> <Loader.Item height="40px" />
</Loader> </Loader>
) : ( ) : (
<div className="divide-y divide-custom-border-200 rounded-[10px] border border-custom-border-200 bg-custom-background-100 px-6"> <div className="divide-y divide-custom-border-200">
{members.length > 0 {members.length > 0
? members.map((member) => ( ? members.map((member) => (
<div key={member.id} className="flex items-center justify-between py-6"> <div
key={member.id}
className="flex items-center justify-between px-3.5 py-[18px]"
>
<div className="flex items-center gap-x-6 gap-y-2"> <div className="flex items-center gap-x-6 gap-y-2">
{member.avatar && member.avatar !== "" ? ( {member.avatar && member.avatar !== "" ? (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white"> <div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
@ -242,7 +397,20 @@ const MembersSettings: NextPage = () => {
</div> </div>
)} )}
<CustomSelect <CustomSelect
label={ROLE[member.role as keyof typeof ROLE]} customButton={
<button className="flex item-center gap-1">
<span
className={`flex items-center text-sm font-medium ${
member.memberId !== user?.id ? "" : "text-custom-sidebar-text-400"
}`}
>
{ROLE[member.role as keyof typeof ROLE]}
</span>
{member.memberId !== user?.id && (
<Icon iconName="expand_more" className="text-lg font-medium" />
)}
</button>
}
value={member.role} value={member.role}
onChange={(value: 5 | 10 | 15 | 20 | undefined) => { onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
if (!activeWorkspace || !projectDetails) return; if (!activeWorkspace || !projectDetails) return;
@ -306,7 +474,11 @@ const MembersSettings: NextPage = () => {
> >
<span className="flex items-center justify-start gap-2"> <span className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" /> <XMarkIcon className="h-4 w-4" />
<span>Remove member</span>
<span>
{" "}
{member.memberId !== user?.id ? "Remove member" : "Leave project"}
</span>
</span> </span>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
</CustomMenu> </CustomMenu>

View File

@ -18,7 +18,7 @@ import {
SingleState, SingleState,
StateGroup, StateGroup,
} from "components/states"; } from "components/states";
import { SettingsHeader } from "components/project"; import { SettingsSidebar } from "components/project";
// ui // ui
import { Loader } from "components/ui"; import { Loader } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -73,31 +73,33 @@ const StatesSettings: NextPage = () => {
</Breadcrumbs> </Breadcrumbs>
} }
> >
<div className="p-8"> <div className="flex flex-row gap-2">
<SettingsHeader /> <div className="w-80 py-8">
<div className="grid grid-cols-12 gap-10"> <SettingsSidebar />
<div className="col-span-12 sm:col-span-5">
<h3 className="text-2xl font-semibold text-custom-text-100">States</h3>
<p className="text-custom-text-200">Manage the states of this project.</p>
</div> </div>
<div className="col-span-12 space-y-8 sm:col-span-7"> <div className="pr-9 py-8 gap-10 w-full">
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">States</h3>
</div>
<div className="space-y-8 py-6">
{states && projectDetails && orderedStateGroups ? ( {states && projectDetails && orderedStateGroups ? (
Object.keys(orderedStateGroups).map((key) => { Object.keys(orderedStateGroups).map((key) => {
if (orderedStateGroups[key].length !== 0) if (orderedStateGroups[key].length !== 0)
return ( return (
<div key={key}> <div key={key} className="flex flex-col gap-2">
<div className="mb-2 flex w-full justify-between"> <div className="flex w-full justify-between">
<h4 className="text-custom-text-200 capitalize">{key}</h4> <h4 className="text-base font-medium text-custom-text-200 capitalize">
{key}
</h4>
<button <button
type="button" type="button"
className="flex items-center gap-2 text-custom-primary-100 hover:text-custom-primary-200 outline-none" className="flex items-center gap-2 text-custom-primary-100 px-2 hover:text-custom-primary-200 outline-none"
onClick={() => setActiveGroup(key as keyof StateGroup)} onClick={() => setActiveGroup(key as keyof StateGroup)}
> >
<PlusIcon className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
Add
</button> </button>
</div> </div>
<div className="divide-y divide-custom-border-200 rounded-[10px] border border-custom-border-200"> <div className="flex flex-col gap-2 rounded">
{key === activeGroup && ( {key === activeGroup && (
<CreateUpdateStateInline <CreateUpdateStateInline
groupLength={orderedStateGroups[key].length} groupLength={orderedStateGroups[key].length}

View File

@ -2,7 +2,7 @@
import APIService from "services/api.service"; import APIService from "services/api.service";
import trackEventServices from "./track-event.service"; import trackEventServices from "./track-event.service";
// types // types
import type { IIssueViewOptions, IModule, IIssue, ICurrentUserResponse } from "types"; import type { IModule, IIssue, ICurrentUserResponse } from "types";
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; const { NEXT_PUBLIC_API_BASE_URL } = process.env;