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
import { CustomSelect, ToggleSwitch } from "components/ui";
import { SelectMonthModal } from "components/automation";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
// icon
import { ArchiveRestore } from "lucide-react";
// constants
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
// types
@ -28,14 +28,18 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
handleClose={() => setmonthModal(false)}
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 items-center justify-between gap-x-8 gap-y-2">
<div className="flex flex-col gap-2.5">
<h4 className="text-lg font-semibold">Auto-archive closed issues</h4>
<p className="text-sm text-custom-text-200">
Plane will automatically archive issues that have been completed or cancelled for the
configured time period.
</p>
<div className="flex flex-col gap-4 border-b border-custom-border-200 px-4 py-6">
<div className="flex items-center justify-between">
<div className="flex items-start gap-3">
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
<ArchiveRestore className="h-4 w-4 text-custom-text-100 flex-shrink-0" />
</div>
<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>
</div>
</div>
<ToggleSwitch
value={projectDetails?.archive_in !== 0}
@ -47,40 +51,43 @@ export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleC
size="sm"
/>
</div>
{projectDetails?.archive_in !== 0 && (
<div className="flex items-center justify-between gap-2 w-full">
<div className="w-1/2 text-base font-medium">
Auto-archive issues that are closed for
</div>
<div className="w-1/2">
<CustomSelect
value={projectDetails?.archive_in}
label={`${projectDetails?.archive_in} ${
projectDetails?.archive_in === 1 ? "Month" : "Months"
}`}
onChange={(val: number) => {
handleChange({ archive_in: val });
}}
input
verticalPosition="top"
width="w-full"
>
<>
{PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.label} value={month.value}>
{month.label}
</CustomSelect.Option>
))}
<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"
onClick={() => setmonthModal(true)}
>
Customise Time Range
</button>
</>
</CustomSelect>
{projectDetails?.archive_in !== 0 && (
<div className="ml-12">
<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
</div>
<div className="w-1/2">
<CustomSelect
value={projectDetails?.archive_in}
label={`${projectDetails?.archive_in} ${
projectDetails?.archive_in === 1 ? "Month" : "Months"
}`}
onChange={(val: number) => {
handleChange({ archive_in: val });
}}
input
verticalPosition="bottom"
width="w-full"
>
<>
{PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.label} value={month.value}>
<span className="text-sm">{month.label}</span>
</CustomSelect.Option>
))}
<button
type="button"
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)}
>
Customise Time Range
</button>
</>
</CustomSelect>
</div>
</div>
</div>
)}

View file

@ -5,11 +5,12 @@ import useSWR from "swr";
import { useRouter } from "next/router";
// component
import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
import { CustomSearchSelect, CustomSelect, Icon, ToggleSwitch } from "components/ui";
import { SelectMonthModal } from "components/automation";
// icons
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import { Squares2X2Icon } from "@heroicons/react/24/outline";
import { StateGroupIcon } from "components/icons";
import { ArchiveX } from "lucide-react";
// services
import stateService from "services/state.service";
// constants
@ -76,14 +77,18 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
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 items-center justify-between gap-x-8 gap-y-2 ">
<div className="flex flex-col gap-2.5">
<h4 className="text-lg font-semibold">Auto-close inactive issues</h4>
<p className="text-sm text-custom-text-200">
Plane will automatically close the issues that have not been updated for the
configured time period.
</p>
<div className="flex flex-col gap-4 border-b border-custom-border-200 px-4 py-6">
<div className="flex items-center justify-between">
<div className="flex items-start gap-3">
<div className="flex items-center justify-center p-3 rounded bg-custom-background-90">
<ArchiveX className="h-4 w-4 text-red-500 flex-shrink-0" />
</div>
<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>
</div>
</div>
<ToggleSwitch
value={projectDetails?.close_in !== 0}
@ -95,82 +100,86 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
size="sm"
/>
</div>
{projectDetails?.close_in !== 0 && (
<div className="flex flex-col gap-4 w-full">
<div className="flex items-center justify-between gap-2 w-full">
<div className="w-1/2 text-base font-medium">
Auto-close issues that are inactive for
<div className="ml-12">
<div className="flex flex-col gap-4">
<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
</div>
<div className="w-1/2">
<CustomSelect
value={projectDetails?.close_in}
label={`${projectDetails?.close_in} ${
projectDetails?.close_in === 1 ? "Month" : "Months"
}`}
onChange={(val: number) => {
handleChange({ close_in: val });
}}
input
width="w-full"
>
<>
{PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.label} value={month.value}>
{month.label}
</CustomSelect.Option>
))}
<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"
onClick={() => setmonthModal(true)}
>
Customise Time Range
</button>
</>
</CustomSelect>
</div>
</div>
<div className="w-1/2">
<CustomSelect
value={projectDetails?.close_in}
label={`${projectDetails?.close_in} ${
projectDetails?.close_in === 1 ? "Month" : "Months"
}`}
onChange={(val: number) => {
handleChange({ close_in: val });
}}
input
width="w-full"
>
<>
{PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.label} value={month.value}>
{month.label}
</CustomSelect.Option>
))}
<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"
onClick={() => setmonthModal(true)}
>
Customise Time Range
</button>
</>
</CustomSelect>
</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="w-1/2 ">
<CustomSearchSelect
value={
projectDetails?.default_state ? projectDetails?.default_state : defaultState
}
label={
<div className="flex items-center gap-2">
{selectedOption ? (
<StateGroupIcon
stateGroup={selectedOption.group}
color={selectedOption.color}
height="16px"
width="16px"
/>
) : currentDefaultState ? (
<StateGroupIcon
stateGroup={currentDefaultState.group}
color={currentDefaultState.color}
height="16px"
width="16px"
/>
) : (
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
)}
{selectedOption?.name
? selectedOption.name
: currentDefaultState?.name ?? (
<span className="text-custom-text-200">State</span>
)}
</div>
}
onChange={(val: string) => {
handleChange({ default_state: val });
}}
options={options}
disabled={!multipleOptions}
width="w-full"
input
/>
<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 ">
<CustomSearchSelect
value={
projectDetails?.default_state ? projectDetails?.default_state : defaultState
}
label={
<div className="flex items-center gap-2">
{selectedOption ? (
<StateGroupIcon
stateGroup={selectedOption.group}
color={selectedOption.color}
height="16px"
width="16px"
/>
) : currentDefaultState ? (
<StateGroupIcon
stateGroup={currentDefaultState.group}
color={currentDefaultState.color}
height="16px"
width="16px"
/>
) : (
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
)}
{selectedOption?.name
? selectedOption.name
: currentDefaultState?.name ?? (
<span className="text-custom-text-200">State</span>
)}
</div>
}
onChange={(val: string) => {
handleChange({ default_state: val });
}}
options={options}
disabled={!multipleOptions}
width="w-full"
input
/>
</div>
</div>
</div>
</div>

View file

@ -20,6 +20,7 @@ import fileService from "services/file.service";
import { Input, Spinner, PrimaryButton, SecondaryButton } from "components/ui";
// hooks
import useWorkspaceDetails from "hooks/use-workspace-details";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
const unsplashEnabled =
process.env.NEXT_PUBLIC_UNSPLASH_ENABLED === "true" ||
@ -67,6 +68,8 @@ export const ImagePickerPopover: React.FC<Props> = ({
fileService.getUnsplashImages(1, searchParams)
);
const imagePickerRef = useRef<HTMLDivElement>(null);
const { workspaceDetails } = useWorkspaceDetails();
const onDrop = useCallback((acceptedFiles: File[]) => {
@ -116,12 +119,14 @@ export const ImagePickerPopover: React.FC<Props> = ({
onChange(images[0].urls.regular);
}, [value, onChange, images]);
useOutsideClickDetector(imagePickerRef, () => setIsOpen(false));
if (!unsplashEnabled) return null;
return (
<Popover className="relative z-[2]" ref={ref}>
<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)}
disabled={disabled}
>
@ -137,7 +142,10 @@ export const ImagePickerPopover: React.FC<Props> = ({
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">
<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>
<div>
<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
import { Tab, Transition, Popover } from "@headlessui/react";
// react colors
import { TwitterPicker } from "react-color";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// types
import { Props } from "./types";
// emojis
@ -36,6 +38,8 @@ const EmojiIconPicker: React.FC<Props> = ({
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
const emojiPickerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setRecentEmojis(getRecentEmojis());
}, []);
@ -44,6 +48,8 @@ const EmojiIconPicker: React.FC<Props> = ({
if (!value || value?.length === 0) onChange(getRandomEmoji());
}, [value, onChange]);
useOutsideClickDetector(emojiPickerRef, () => setIsOpen(false));
return (
<Popover className="relative z-[1]">
<Popover.Button
@ -63,7 +69,10 @@ const EmojiIconPicker: React.FC<Props> = ({
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">
<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.List className="flex-0 -mx-2 flex justify-around gap-1 p-1">
{tabOptions.map((tab) => (

View file

@ -66,7 +66,7 @@ export const SingleEstimate: React.FC<Props> = ({
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>
<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 "./single-comment-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>,
})) ?? [];
if (userRepositories.length < 1) return null;
return (
<CustomSearchSelect
value={value}

View file

@ -83,9 +83,7 @@ export const SelectChannel: React.FC<Props> = ({ integration }) => {
{projectIntegration ? (
<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 ${
slackChannelAvailabilityToggle ? "bg-green-500" : "bg-gray-200"
}`}
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`}
role="switch"
aria-checked
onClick={() => {
@ -94,8 +92,8 @@ export const SelectChannel: React.FC<Props> = ({ integration }) => {
>
<span
aria-hidden="true"
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
slackChannelAvailabilityToggle ? "translate-x-5" : "translate-x-0"
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-3" : "translate-x-0"
}`}
/>
</button>

View file

@ -17,7 +17,7 @@ import issuesService from "services/issues.service";
// ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { Component } from "lucide-react";
// types
import { IIssueLabels } from "types";
// fetch-keys
@ -132,7 +132,7 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
return (
<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"
}`}
ref={ref}
@ -146,18 +146,12 @@ export const CreateUpdateLabelInline = forwardRef<HTMLDivElement, Props>(
open ? "text-custom-text-100" : "text-custom-text-200"
}`}
>
<span
className="h-5 w-5 rounded"
<Component
className="h-4 w-4 text-custom-text-100 flex-shrink-0"
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>
<Transition

View file

@ -13,12 +13,12 @@ import { CustomMenu } from "components/ui";
// icons
import {
ChevronDownIcon,
RectangleGroupIcon,
XMarkIcon,
PlusIcon,
PencilIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { Component, X } from "lucide-react";
// types
import { ICurrentUserResponse, IIssueLabels } from "types";
// fetch-keys
@ -76,20 +76,18 @@ export const SingleLabelGroup: React.FC<Props> = ({
return (
<Disclosure
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
>
{({ open }) => (
<>
<div className="flex cursor-pointer items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span>
<RectangleGroupIcon className="h-4 w-4" />
</span>
<Component className="h-4 w-4 text-custom-text-100 flex-shrink-0" />
<h6>{label.name}</h6>
</div>
<div className="flex items-center gap-2">
<CustomMenu ellipsis>
<CustomMenu ellipsis buttonClassName="!text-custom-sidebar-text-400">
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
<span className="flex items-center justify-start gap-2">
<PlusIcon className="h-4 w-4" />
@ -112,7 +110,9 @@ export const SingleLabelGroup: React.FC<Props> = ({
<Disclosure.Button>
<span>
<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>
</Disclosure.Button>
@ -128,15 +128,15 @@ export const SingleLabelGroup: React.FC<Props> = ({
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="mt-3 ml-6 space-y-3">
<div className="mt-2.5 ml-6">
{labelChildren.map((child) => (
<div
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">
<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={{
backgroundColor:
child.color && child.color !== "" ? child.color : "#000000",
@ -144,27 +144,38 @@ export const SingleLabelGroup: React.FC<Props> = ({
/>
{child.name}
</h5>
<div className="pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
<CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => removeFromGroup(child)}>
<span className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>Remove from group</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => editLabel(child)}>
<span className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit label</span>
</span>
</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>
<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={() => removeFromGroup(child)}>
<span className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>Remove from group</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => editLabel(child)}>
<span className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit label</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</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>
))}

View file

@ -5,7 +5,8 @@ import { CustomMenu } from "components/ui";
// types
import { IIssueLabels } from "types";
//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 = {
label: IIssueLabels;
@ -20,8 +21,8 @@ export const SingleLabel: React.FC<Props> = ({
editLabel,
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="flex items-center justify-between">
<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="group flex items-center justify-between">
<div className="flex items-center gap-3">
<span
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
@ -31,26 +32,36 @@ export const SingleLabel: React.FC<Props> = ({
/>
<h6 className="text-sm">{label.name}</h6>
</div>
<CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
<span className="flex items-center justify-start gap-2">
<RectangleGroupIcon className="h-4 w-4" />
<span>Convert to group</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => editLabel(label)}>
<span className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit label</span>
</span>
</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>
<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)}>
<span className="flex items-center justify-start gap-2">
<RectangleGroupIcon className="h-4 w-4" />
<span>Convert to group</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => editLabel(label)}>
<span className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit label</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</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>
);

View file

@ -1,8 +1,9 @@
export * from "./create-project-modal";
export * from "./delete-project-modal";
export * from "./sidebar-list";
export * from "./settings-header";
export * from "./settings-sidebar";
export * from "./single-integration-card";
export * from "./single-project-card";
export * from "./single-sidebar-project";
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: {
logo: SlackLogo,
description:
"Connect your slack channel to this project to get regular updates. Control which notification you want to receive.",
description: "Get regular updates and control which notification you want to receive.",
},
};
@ -93,19 +92,19 @@ export const SingleIntegration: React.FC<Props> = ({ integration }) => {
return (
<>
{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="h-12 w-12 flex-shrink-0">
<div className="h-10 w-10 flex-shrink-0">
<Image
src={integrationDetails[integration.integration_detail.provider].logo}
alt={`${integration.integration_detail.title} Logo`}
/>
</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}
</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}
</p>
</div>

View file

@ -9,13 +9,10 @@ import stateService from "services/state.service";
// ui
import { Tooltip } from "components/ui";
// icons
import {
ArrowDownIcon,
ArrowUpIcon,
PencilSquareIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { ArrowDownIcon, ArrowUpIcon } from "@heroicons/react/24/outline";
import { StateGroupIcon } from "components/icons";
import { Pencil, X } from "lucide-react";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
import { groupBy, orderArrayBy } from "helpers/array.helper";
@ -160,15 +157,15 @@ export const SingleState: React.FC<Props> = ({
};
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">
<StateGroupIcon stateGroup={state.group} color={state.color} height="20px" width="20px" />
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
<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>
</div>
</div>
<div className="flex items-center gap-2">
<div className="group flex items-center gap-2.5">
{index !== 0 && (
<button
type="button"
@ -192,37 +189,43 @@ export const SingleState: React.FC<Props> = ({
) : (
<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}
disabled={isSubmitting}
>
Set as default
Mark as default
</button>
)}
<div className=" items-center gap-2.5 hidden group-hover:flex">
<button
type="button"
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="grid place-items-center" onClick={handleEditState}>
<PencilSquareIcon className="h-4 w-4 text-custom-text-200" />
</button>
<button
type="button"
className={`${
state.default || groupLength === 1 ? "cursor-not-allowed" : ""
} grid place-items-center`}
onClick={handleDeleteState}
disabled={state.default || groupLength === 1}
>
{state.default ? (
<Tooltip tooltipContent="Cannot delete the default state.">
<TrashIcon className="h-4 w-4 text-red-500" />
</Tooltip>
) : groupLength === 1 ? (
<Tooltip tooltipContent="Cannot have an empty group.">
<TrashIcon className="h-4 w-4 text-red-500" />
</Tooltip>
) : (
<TrashIcon className="h-4 w-4 text-red-500" />
)}
</button>
<button
type="button"
className={`group-hover:opacity-100 opacity-0 ${
state.default || groupLength === 1 ? "cursor-not-allowed" : ""
} grid place-items-center`}
onClick={handleDeleteState}
disabled={state.default || groupLength === 1}
>
{state.default ? (
<Tooltip tooltipContent="Cannot delete the default state.">
<X className="h-3.5 w-3.5 text-red-500" />
</Tooltip>
) : groupLength === 1 ? (
<Tooltip tooltipContent="Cannot have an empty group.">
<X className="h-3.5 w-3.5 text-red-500" />
</Tooltip>
) : (
<X className="h-3.5 w-3.5 text-red-500" />
)}
</button>
</div>
</div>
</div>
);

View file

@ -6,8 +6,8 @@ type Props = {
};
export const IntegrationAndImportExportBanner: React.FC<Props> = ({ bannerName, description }) => (
<div className="flex flex-col items-start gap-3">
<h3 className="text-2xl font-semibold">{bannerName}</h3>
<div className="flex flex-col items-start gap-3 py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">{bannerName}</h3>
{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">
<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}
onChange={onChange}
className={`relative flex-shrink-0 inline-flex ${
size === "sm" ? "h-3.5 w-6" : size === "md" ? "h-4 w-7" : "h-6 w-11"
} flex-shrink-0 cursor-pointer rounded-full border-2 border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
value ? "bg-green-500" : "bg-custom-background-80"
size === "sm" ? "h-4 w-6" : size === "md" ? "h-5 w-8" : "h-6 w-10"
} flex-shrink-0 cursor-pointer rounded-full border border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
value ? "bg-custom-primary-100" : "bg-gray-700"
} ${className || ""}`}
>
<span className="sr-only">{label}</span>
<span
aria-hidden="true"
className={`inline-block ${
size === "sm" ? "h-2.5 w-2.5" : size === "md" ? "h-3 w-3" : "h-5 w-5"
className={`self-center inline-block ${
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 ${
value
? (size === "sm"
? "translate-x-2.5"
: size === "md"
? "translate-x-3"
: size === "md"
? "translate-x-4"
: "translate-x-5") + " bg-white"
: "translate-x-0 bg-custom-background-90"
: "translate-x-1 bg-custom-background-90"
}`}
/>
</Switch>