chore: estimate dropdown handled in issues

This commit is contained in:
guru_sainath 2024-05-31 16:01:37 +05:30
parent 78db7fe03f
commit 2adb18f636
16 changed files with 99 additions and 171 deletions

View File

@ -15,7 +15,7 @@ export type TIssue = {
priority: TIssuePriorities; priority: TIssuePriorities;
label_ids: string[]; label_ids: string[];
assignee_ids: string[]; assignee_ids: string[];
estimate_point: number | null; estimate_point: string | null;
sub_issues_count: number; sub_issues_count: number;
attachment_count: number; attachment_count: number;

View File

@ -5,7 +5,7 @@ import { Controller, useForm } from "react-hook-form"; // services
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
// ui // ui
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import { Popover, PopoverButton, PopoverPanel, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor"; import { RichTextReadOnlyEditor } from "@/components/editor/rich-text-editor/rich-text-read-only-editor";
// icons // icons
@ -178,9 +178,9 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
return ( return (
<Popover as="div" className={`relative w-min text-left`}> <Popover as="div" className={`relative w-min text-left`}>
<PopoverButton as={Fragment}> <Popover.Button as={Fragment}>
<button ref={setReferenceElement}>{button}</button> <button ref={setReferenceElement}>{button}</button>
</PopoverButton> </Popover.Button>
<Transition <Transition
show={isOpen} show={isOpen}
as={React.Fragment} as={React.Fragment}
@ -191,7 +191,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<PopoverPanel <Popover.Panel
as="div" as="div"
className={`fixed z-10 flex w-full min-w-[50rem] max-w-full flex-col space-y-4 overflow-hidden rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${className}`} className={`fixed z-10 flex w-full min-w-[50rem] max-w-full flex-col space-y-4 overflow-hidden rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${className}`}
ref={setPopperElement as Ref<HTMLDivElement>} ref={setPopperElement as Ref<HTMLDivElement>}
@ -261,7 +261,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
</Button> </Button>
</div> </div>
</div> </div>
</PopoverPanel> </Popover.Panel>
</Transition> </Transition>
</Popover> </Popover>
); );

View File

@ -1,5 +1,4 @@
import { Fragment, ReactNode, useRef, useState } from "react"; import { Fragment, ReactNode, useRef, useState } from "react";
import sortBy from "lodash/sortBy";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { Check, ChevronDown, Search, Triangle } from "lucide-react"; import { Check, ChevronDown, Search, Triangle } from "lucide-react";
@ -9,6 +8,8 @@ import { cn } from "@/helpers/common.helper";
// hooks // hooks
import { import {
useAppRouter, useAppRouter,
useEstimate,
useProjectEstimates,
// useEstimate // useEstimate
} from "@/hooks/store"; } from "@/hooks/store";
import { useDropdown } from "@/hooks/use-dropdown"; import { useDropdown } from "@/hooks/use-dropdown";
@ -22,15 +23,15 @@ type Props = TDropdownProps & {
button?: ReactNode; button?: ReactNode;
dropdownArrow?: boolean; dropdownArrow?: boolean;
dropdownArrowClassName?: string; dropdownArrowClassName?: string;
onChange: (val: number | null) => void; onChange: (val: string | undefined) => void;
onClose?: () => void; onClose?: () => void;
projectId: string; projectId: string;
value: number | null; value: string | undefined;
}; };
type DropdownOptions = type DropdownOptions =
| { | {
value: number | null; value: string | null;
query: string; query: string;
content: JSX.Element; content: JSX.Element;
}[] }[]
@ -79,23 +80,29 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
}); });
// store hooks // store hooks
const { workspaceSlug } = useAppRouter(); const { workspaceSlug } = useAppRouter();
console.log("workspaceSlug", workspaceSlug);
console.log("projectId", projectId);
// const { fetchProjectEstimates, getProjectActiveEstimateDetails, getEstimatePointValue } = useEstimate(); const { currentActiveEstimateId, getProjectEstimates } = useProjectEstimates();
// const activeEstimate = getProjectActiveEstimateDetails(projectId); const { estimatePointIds, estimatePointById } = useEstimate(
const activeEstimate: any = undefined; currentActiveEstimateId ? currentActiveEstimateId : undefined
);
const options: DropdownOptions = sortBy(activeEstimate?.points ?? [], "key")?.map((point) => ({ const options: DropdownOptions = (estimatePointIds ?? [])
value: point.key, ?.map((estimatePoint) => {
query: `${point?.value}`, const currentEstimatePoint = estimatePointById(estimatePoint);
content: ( if (currentEstimatePoint)
<div className="flex items-center gap-2"> return {
<Triangle className="h-3 w-3 flex-shrink-0" /> value: currentEstimatePoint.id,
<span className="flex-grow truncate">{point.value}</span> query: `${currentEstimatePoint?.value}`,
</div> content: (
), <div className="flex items-center gap-2">
})); <Triangle className="h-3 w-3 flex-shrink-0" />
<span className="flex-grow truncate">{currentEstimatePoint.value}</span>
</div>
),
};
else undefined;
})
.filter((estimatePointDropdownOption) => estimatePointDropdownOption != undefined) as DropdownOptions;
options?.unshift({ options?.unshift({
value: null, value: null,
query: "No estimate", query: "No estimate",
@ -110,14 +117,10 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
const filteredOptions = const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
const selectedEstimate = const selectedEstimate = value && estimatePointById ? estimatePointById(value) : undefined;
value !== null
? // getEstimatePointValue(value, projectId)
null
: null;
const onOpen = async () => { const onOpen = async () => {
// if (!activeEstimate && workspaceSlug) await fetchProjectEstimates(workspaceSlug, projectId); if (!currentActiveEstimateId && workspaceSlug) await getProjectEstimates(workspaceSlug, projectId);
}; };
const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({ const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({
@ -131,7 +134,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
setQuery, setQuery,
}); });
const dropdownOnChange = (val: number | null) => { const dropdownOnChange = (val: string | undefined) => {
onChange(val); onChange(val);
handleClose(); handleClose();
}; };
@ -175,13 +178,13 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
className={buttonClassName} className={buttonClassName}
isActive={isOpen} isActive={isOpen}
tooltipHeading="Estimate" tooltipHeading="Estimate"
tooltipContent={selectedEstimate !== null ? selectedEstimate : placeholder} tooltipContent={selectedEstimate ? selectedEstimate?.value : placeholder}
showTooltip={showTooltip} showTooltip={showTooltip}
variant={buttonVariant} variant={buttonVariant}
> >
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />} {!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
{(selectedEstimate || placeholder) && BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( {(selectedEstimate || placeholder) && BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate">{selectedEstimate !== null ? selectedEstimate : placeholder}</span> <span className="flex-grow truncate">{selectedEstimate ? selectedEstimate?.value : placeholder}</span>
)} )}
{dropdownArrow && ( {dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" /> <ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
@ -215,20 +218,14 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
{filteredOptions ? ( {filteredOptions ? (
filteredOptions.length > 0 ? ( filteredOptions.length > 0 ? (
filteredOptions.map((option) => ( filteredOptions.map((option) => (
<Combobox.Option <Combobox.Option key={option.value} value={option.value}>
key={option.value} {({ active, selected }) => (
value={option.value} <div
className={({ active, selected }) => className={`flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${active ? `!hover:bg-custom-background-80` : ``} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`}
`flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${ >
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span> <span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />} {selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</> </div>
)} )}
</Combobox.Option> </Combobox.Option>
)) ))

View File

@ -1,6 +1,6 @@
import { Ref, useState } from "react"; import { Ref, useState } from "react";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import { Popover } from "@headlessui/react";
// popper // popper
// helper // helper
import { getButtonStyling } from "@plane/ui"; import { getButtonStyling } from "@plane/ui";
@ -42,7 +42,7 @@ export const ComicBoxButton: React.FC<Props> = (props) => {
return ( return (
<Popover> <Popover>
<PopoverButton ref={setReferenceElement} onClick={onClick} disabled={disabled}> <Popover.Button ref={setReferenceElement} onClick={onClick} disabled={disabled}>
<div className={`flex items-center gap-2.5 ${getButtonStyling("primary", "lg", disabled)}`}> <div className={`flex items-center gap-2.5 ${getButtonStyling("primary", "lg", disabled)}`}>
{icon} {icon}
<span className="leading-4">{label}</span> <span className="leading-4">{label}</span>
@ -55,9 +55,9 @@ export const ComicBoxButton: React.FC<Props> = (props) => {
<div className={`absolute bg-blue-400/40 right-0 h-1.5 w-1.5 mt-0.5 mr-0.5 rounded-full`} /> <div className={`absolute bg-blue-400/40 right-0 h-1.5 w-1.5 mt-0.5 mr-0.5 rounded-full`} />
</span> </span>
</div> </div>
</PopoverButton> </Popover.Button>
{isHovered && ( {isHovered && (
<PopoverPanel <Popover.Panel
as="div" as="div"
className="flex flex-col rounded border border-custom-border-200 bg-custom-background-100 p-5 relative min-w-80" className="flex flex-col rounded border border-custom-border-200 bg-custom-background-100 p-5 relative min-w-80"
ref={setPopperElement as Ref<HTMLDivElement>} ref={setPopperElement as Ref<HTMLDivElement>}
@ -68,7 +68,7 @@ export const ComicBoxButton: React.FC<Props> = (props) => {
<div className="absolute w-2 h-2 bg-custom-background-100 border rounded-lb-sm border-custom-border-200 border-r-0 border-t-0 transform rotate-45 bottom-2 -left-[5px]" /> <div className="absolute w-2 h-2 bg-custom-background-100 border rounded-lb-sm border-custom-border-200 border-r-0 border-t-0 transform rotate-45 bottom-2 -left-[5px]" />
<h3 className="text-lg font-semibold w-full">{title}</h3> <h3 className="text-lg font-semibold w-full">{title}</h3>
<h4 className="mt-1 text-sm">{description}</h4> <h4 className="mt-1 text-sm">{description}</h4>
</PopoverPanel> </Popover.Panel>
)} )}
</Popover> </Popover>
); );

View File

@ -1,6 +1,6 @@
import { FC, useRef, Fragment, useState } from "react"; import { FC, useRef, Fragment, useState } from "react";
import { Info, Check, ChevronDown } from "lucide-react"; import { Info, Check, ChevronDown } from "lucide-react";
import { Listbox, ListboxButton, ListboxOptions, Transition, ListboxOption } from "@headlessui/react"; import { Listbox, Transition } from "@headlessui/react";
import { TEstimatePointsObject } from "@plane/types"; import { TEstimatePointsObject } from "@plane/types";
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// helpers // helpers
@ -51,7 +51,7 @@ export const EstimatePointDropdown: FC<TEstimatePointDropdown> = (props) => {
}} }}
className="w-full flex-shrink-0 text-left" className="w-full flex-shrink-0 text-left"
> >
<ListboxButton <Listbox.Button
type="button" type="button"
ref={buttonRef} ref={buttonRef}
onClick={() => setIsDropdownOpen((prev) => !prev)} onClick={() => setIsDropdownOpen((prev) => !prev)}
@ -75,7 +75,7 @@ export const EstimatePointDropdown: FC<TEstimatePointDropdown> = (props) => {
</Tooltip> </Tooltip>
</> </>
)} )}
</ListboxButton> </Listbox.Button>
<Transition <Transition
show={isDropdownOpen} show={isDropdownOpen}
as={Fragment} as={Fragment}
@ -86,12 +86,12 @@ export const EstimatePointDropdown: FC<TEstimatePointDropdown> = (props) => {
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<ListboxOptions <Listbox.Options
ref={dropdownRef} ref={dropdownRef}
className="fixed z-10 mt-1 h-fit w-48 sm:w-60 overflow-y-auto rounded-md border border-custom-border-200 bg-custom-background-100 shadow-sm focus:outline-none" className="fixed z-10 mt-1 h-fit w-48 sm:w-60 overflow-y-auto rounded-md border border-custom-border-200 bg-custom-background-100 shadow-sm focus:outline-none"
> >
<div className="p-1.5"> <div className="p-1.5">
<ListboxOption <Listbox.Option
value={"none"} value={"none"}
className={cn( className={cn(
`cursor-pointer select-none truncate rounded px-1 py-1.5 hover:bg-custom-background-90`, `cursor-pointer select-none truncate rounded px-1 py-1.5 hover:bg-custom-background-90`,
@ -102,9 +102,9 @@ export const EstimatePointDropdown: FC<TEstimatePointDropdown> = (props) => {
<div className="text-sm font-medium w-full line-clamp-1">None</div> <div className="text-sm font-medium w-full line-clamp-1">None</div>
{selectedOption === "none" && <Check size={12} />} {selectedOption === "none" && <Check size={12} />}
</div> </div>
</ListboxOption> </Listbox.Option>
{options.map((option) => ( {options.map((option) => (
<ListboxOption <Listbox.Option
key={option?.key} key={option?.key}
value={option?.id} value={option?.id}
className={cn( className={cn(
@ -116,10 +116,10 @@ export const EstimatePointDropdown: FC<TEstimatePointDropdown> = (props) => {
<div className="text-sm font-medium w-full line-clamp-1">{option.value}</div> <div className="text-sm font-medium w-full line-clamp-1">{option.value}</div>
{selectedOption === option?.id && <Check size={12} />} {selectedOption === option?.id && <Check size={12} />}
</div> </div>
</ListboxOption> </Listbox.Option>
))} ))}
</div> </div>
</ListboxOptions> </Listbox.Options>
</Transition> </Transition>
</Listbox> </Listbox>
</div> </div>

View File

@ -69,7 +69,7 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
let isEstimateValid = false; let isEstimateValid = false;
const currentEstimatePointValues = estimatePoints const currentEstimatePointValues = estimatePoints
.map((point) => point?.value || undefined) .map((point) => (point?.id != estimatePoint?.id ? point?.value : undefined))
.filter((value) => value != undefined) as string[]; .filter((value) => value != undefined) as string[];
const isRepeated = const isRepeated =
(estimateType && isEstimatePointValuesRepeated(currentEstimatePointValues, estimateType, estimateInputValue)) || (estimateType && isEstimatePointValuesRepeated(currentEstimatePointValues, estimateType, estimateInputValue)) ||

View File

@ -142,10 +142,10 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
)} )}
{/* estimate */} {/* estimate */}
{isVisible && areEstimateEnabledByProjectId(projectId) && ( {isVisible && projectId && areEstimateEnabledByProjectId(projectId) && (
<div className="h-7"> <div className="h-7">
<EstimateDropdown <EstimateDropdown
value={data?.estimate_point || null} value={data?.estimate_point || undefined}
onChange={(estimatePoint) => handleData("estimate_point", estimatePoint)} onChange={(estimatePoint) => handleData("estimate_point", estimatePoint)}
projectId={projectId} projectId={projectId}
buttonVariant="border-with-text" buttonVariant="border-with-text"

View File

@ -54,13 +54,7 @@ import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
import { copyTextToClipboard } from "@/helpers/string.helper"; import { copyTextToClipboard } from "@/helpers/string.helper";
// types // types
import { import { useProjectEstimates, useIssueDetail, useProject, useProjectState, useUser } from "@/hooks/store";
// useEstimate,
useIssueDetail,
useProject,
useProjectState,
useUser,
} from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// components // components
import type { TIssueOperations } from "./root"; import type { TIssueOperations } from "./root";
@ -88,8 +82,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
// store hooks // store hooks
const { getProjectById } = useProject(); const { getProjectById } = useProject();
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
// const { areEstimatesEnabledForCurrentProject } = useEstimate(); const { areEstimateEnabledByProjectId } = useProjectEstimates();
const areEstimatesEnabledForCurrentProject = false;
const { const {
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail();
@ -319,15 +312,17 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
/> />
</div> </div>
{areEstimatesEnabledForCurrentProject && ( {projectId && areEstimateEnabledByProjectId(projectId) && (
<div className="flex h-8 items-center gap-2"> <div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300"> <div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<Triangle className="h-4 w-4 flex-shrink-0" /> <Triangle className="h-4 w-4 flex-shrink-0" />
<span>Estimate</span> <span>Estimate</span>
</div> </div>
<EstimateDropdown <EstimateDropdown
value={issue?.estimate_point !== null ? issue.estimate_point : null} value={issue?.estimate_point ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })} onChange={(val: string | undefined) =>
issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })
}
projectId={projectId} projectId={projectId}
disabled={!isEditable} disabled={!isEditable}
buttonVariant="transparent-with-text" buttonVariant="transparent-with-text"

View File

@ -220,7 +220,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
); );
}; };
const handleEstimate = (value: number | null) => { const handleEstimate = (value: string | undefined) => {
updateIssue && updateIssue &&
updateIssue(issue.project_id, issue.id, { estimate_point: value }).then(() => { updateIssue(issue.project_id, issue.id, { estimate_point: value }).then(() => {
captureIssueEvent({ captureIssueEvent({
@ -398,7 +398,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="estimate"> <WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="estimate">
<div className="h-5" onClick={handleEventPropagation}> <div className="h-5" onClick={handleEventPropagation}>
<EstimateDropdown <EstimateDropdown
value={issue.estimate_point} value={issue.estimate_point ?? undefined}
onChange={handleEstimate} onChange={handleEstimate}
projectId={issue.project_id} projectId={issue.project_id}
disabled={isReadOnly} disabled={isReadOnly}

View File

@ -17,7 +17,7 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = observer((props: Props
return ( return (
<div className="h-11 border-b-[0.5px] border-custom-border-200"> <div className="h-11 border-b-[0.5px] border-custom-border-200">
<EstimateDropdown <EstimateDropdown
value={issue.estimate_point} value={issue.estimate_point || undefined}
onChange={(data) => onChange={(data) =>
onChange(issue, { estimate_point: data }, { changed_property: "estimate_point", change_details: data }) onChange(issue, { estimate_point: data }, { changed_property: "estimate_point", change_details: data })
} }

View File

@ -673,7 +673,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<div className="h-7"> <div className="h-7">
<EstimateDropdown <EstimateDropdown
value={value} value={value || undefined}
onChange={(estimatePoint) => { onChange={(estimatePoint) => {
onChange(estimatePoint); onChange(estimatePoint);
handleFormChange(); handleFormChange();

View File

@ -197,14 +197,14 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<span>Estimate</span> <span>Estimate</span>
</div> </div>
<EstimateDropdown <EstimateDropdown
value={issue?.estimate_point !== null ? issue.estimate_point : null} value={issue.estimate_point ?? undefined}
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })} onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })}
projectId={projectId} projectId={projectId}
disabled={disabled} disabled={disabled}
buttonVariant="transparent-with-text" buttonVariant="transparent-with-text"
className="w-3/4 flex-grow group" className="w-3/4 flex-grow group"
buttonContainerClassName="w-full text-left" buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm ${issue?.estimate_point !== null ? "" : "text-custom-text-400"}`} buttonClassName={`text-sm ${issue?.estimate_point !== undefined ? "" : "text-custom-text-400"}`}
placeholder="None" placeholder="None"
hideIcon hideIcon
dropdownArrow dropdownArrow

View File

@ -1,64 +0,0 @@
import React from "react";
import { Field, Label, Radio, RadioGroup } from "@headlessui/react";
// helpers
import { cn } from "@/helpers/common.helper";
type RadioInputProps = {
label: string | React.ReactNode | undefined;
labelClassName?: string;
ariaLabel?: string;
options: { label: string; value: string; disabled?: boolean }[];
vertical?: boolean;
selected: string;
onChange: (value: string) => void;
className?: string;
};
const RadioInput = ({
label: inputLabel,
labelClassName: inputLabelClassName,
options,
vertical,
selected,
ariaLabel,
onChange,
className,
}: RadioInputProps) => {
const wrapperClass = vertical ? "flex flex-col gap-1" : "flex gap-2";
const setSelected = (value: string) => {
onChange(value);
};
let aria = ariaLabel ? ariaLabel.toLowerCase().replace(" ", "-") : "";
if (!aria && typeof inputLabel === "string") {
aria = inputLabel.toLowerCase().replace(" ", "-");
} else {
aria = "radio-input";
}
return (
<RadioGroup value={selected} onChange={setSelected} aria-label={aria} className={className}>
<Label className={cn(`mb-2`, inputLabelClassName)}>{inputLabel}</Label>
<div className={`${wrapperClass}`}>
{options.map(({ value, label, disabled }) => (
<Field key={label} className="flex items-center gap-2">
<Radio
value={value}
className="group flex size-5 items-center justify-center rounded-full border border-custom-border-400 bg-custom-background-500 data-[checked]:bg-custom-primary-200 data-[checked]:border-custom-primary-100 cursor-pointer
data-[disabled]:bg-custom-background-200
data-[disabled]:border-custom-border-200
data-[disabled]:cursor-not-allowed"
disabled={disabled}
>
<span className="invisible size-2 rounded-full bg-white group-data-[checked]:visible" />
</Radio>
<Label className="text-base cursor-pointer">{label}</Label>
</Field>
))}
</div>
</RadioGroup>
);
};
export { RadioInput };

View File

@ -6,7 +6,7 @@ import { usePopper } from "react-popper";
// icons // icons
import { Activity, Check, ChevronDown, LogOut, Mails, PlusSquare, Settings } from "lucide-react"; import { Activity, Check, ChevronDown, LogOut, Mails, PlusSquare, Settings } from "lucide-react";
// ui // ui
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
// types // types
import { IWorkspace } from "@plane/types"; import { IWorkspace } from "@plane/types";
// plane ui // plane ui
@ -104,7 +104,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
<Menu as="div" className="relative h-full flex-grow truncate text-left"> <Menu as="div" className="relative h-full flex-grow truncate text-left">
{({ open }) => ( {({ open }) => (
<> <>
<MenuButton className="group/menu-button h-full w-full truncate rounded-md text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:outline-none"> <Menu.Button className="group/menu-button h-full w-full truncate rounded-md text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:outline-none">
<div <div
className={`flex items-center gap-x-2 truncate rounded p-1 ${ className={`flex items-center gap-x-2 truncate rounded p-1 ${
sidebarCollapsed ? "justify-center" : "justify-between" sidebarCollapsed ? "justify-center" : "justify-between"
@ -126,7 +126,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
/> />
)} )}
</div> </div>
</MenuButton> </Menu.Button>
<Transition <Transition
as={Fragment} as={Fragment}
enter="transition ease-out duration-100" enter="transition ease-out duration-100"
@ -136,7 +136,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<MenuItems as={Fragment}> <Menu.Items as={Fragment}>
<div className="fixed left-4 z-20 mt-1 flex w-full max-w-[19rem] origin-top-left flex-col divide-y divide-custom-border-100 rounded-md border-[0.5px] border-custom-sidebar-border-300 bg-custom-sidebar-background-100 shadow-custom-shadow-rg outline-none"> <div className="fixed left-4 z-20 mt-1 flex w-full max-w-[19rem] origin-top-left flex-col divide-y divide-custom-border-100 rounded-md border-[0.5px] border-custom-sidebar-border-300 bg-custom-sidebar-background-100 shadow-custom-shadow-rg outline-none">
<div className="vertical-scrollbar scrollbar-sm mb-2 flex max-h-96 flex-col items-start justify-start gap-2 overflow-y-scroll px-4"> <div className="vertical-scrollbar scrollbar-sm mb-2 flex max-h-96 flex-col items-start justify-start gap-2 overflow-y-scroll px-4">
<h6 className="sticky top-0 z-10 h-full w-full bg-custom-sidebar-background-100 pb-1 pt-3 text-sm font-medium text-custom-sidebar-text-400"> <h6 className="sticky top-0 z-10 h-full w-full bg-custom-sidebar-background-100 pb-1 pt-3 text-sm font-medium text-custom-sidebar-text-400">
@ -155,7 +155,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
}} }}
className="w-full" className="w-full"
> >
<MenuItem <Menu.Item
as="div" as="div"
className="flex items-center justify-between gap-1 rounded p-1 text-sm text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80" className="flex items-center justify-between gap-1 rounded p-1 text-sm text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80"
> >
@ -188,7 +188,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
<Check className="h-5 w-5 text-custom-sidebar-text-100" /> <Check className="h-5 w-5 text-custom-sidebar-text-100" />
</span> </span>
)} )}
</MenuItem> </Menu.Item>
</Link> </Link>
))} ))}
</div> </div>
@ -203,13 +203,13 @@ export const WorkspaceSidebarDropdown = observer(() => {
</div> </div>
<div className="flex w-full flex-col items-start justify-start gap-2 px-4 py-2 text-sm"> <div className="flex w-full flex-col items-start justify-start gap-2 px-4 py-2 text-sm">
<Link href="/create-workspace" className="w-full"> <Link href="/create-workspace" className="w-full">
<MenuItem <Menu.Item
as="div" as="div"
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80" className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-100 hover:bg-custom-sidebar-background-80"
> >
<PlusSquare strokeWidth={1.75} className="h-4 w-4 flex-shrink-0" /> <PlusSquare strokeWidth={1.75} className="h-4 w-4 flex-shrink-0" />
Create workspace Create workspace
</MenuItem> </Menu.Item>
</Link> </Link>
{userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => ( {userLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
<Link <Link
@ -220,18 +220,18 @@ export const WorkspaceSidebarDropdown = observer(() => {
if (index > 0) handleItemClick(); if (index > 0) handleItemClick();
}} }}
> >
<MenuItem <Menu.Item
as="div" as="div"
className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80" className="flex items-center gap-2 rounded px-2 py-1 text-sm font-medium text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
> >
<link.icon className="h-4 w-4 flex-shrink-0" /> <link.icon className="h-4 w-4 flex-shrink-0" />
{link.name} {link.name}
</MenuItem> </Menu.Item>
</Link> </Link>
))} ))}
</div> </div>
<div className="w-full px-4 py-2"> <div className="w-full px-4 py-2">
<MenuItem <Menu.Item
as="button" as="button"
type="button" type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1 text-sm font-medium text-red-600 hover:bg-custom-sidebar-background-80" className="flex w-full items-center gap-2 rounded px-2 py-1 text-sm font-medium text-red-600 hover:bg-custom-sidebar-background-80"
@ -239,17 +239,17 @@ export const WorkspaceSidebarDropdown = observer(() => {
> >
<LogOut className="h-4 w-4 flex-shrink-0" /> <LogOut className="h-4 w-4 flex-shrink-0" />
Sign out Sign out
</MenuItem> </Menu.Item>
</div> </div>
</div> </div>
</MenuItems> </Menu.Items>
</Transition> </Transition>
</> </>
)} )}
</Menu> </Menu>
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<Menu as="div" className="relative flex-shrink-0"> <Menu as="div" className="relative flex-shrink-0">
<MenuButton className="grid place-items-center outline-none" ref={setReferenceElement}> <Menu.Button className="grid place-items-center outline-none" ref={setReferenceElement}>
<Avatar <Avatar
name={currentUser?.display_name} name={currentUser?.display_name}
src={currentUser?.avatar || undefined} src={currentUser?.avatar || undefined}
@ -257,7 +257,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
shape="square" shape="square"
className="!text-base" className="!text-base"
/> />
</MenuButton> </Menu.Button>
<Transition <Transition
as={Fragment} as={Fragment}
enter="transition ease-out duration-100" enter="transition ease-out duration-100"
@ -267,7 +267,7 @@ export const WorkspaceSidebarDropdown = observer(() => {
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<MenuItems <Menu.Items
className="absolute left-0 z-20 mt-1 flex w-52 origin-top-left flex-col divide-y className="absolute left-0 z-20 mt-1 flex w-52 origin-top-left flex-col divide-y
divide-custom-sidebar-border-200 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none" divide-custom-sidebar-border-200 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none"
ref={setPopperElement as Ref<HTMLDivElement>} ref={setPopperElement as Ref<HTMLDivElement>}
@ -284,17 +284,17 @@ export const WorkspaceSidebarDropdown = observer(() => {
if (index == 0) handleItemClick(); if (index == 0) handleItemClick();
}} }}
> >
<MenuItem key={index} as="div"> <Menu.Item key={index} as="div">
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"> <span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
<link.icon className="h-4 w-4 stroke-[1.5]" /> <link.icon className="h-4 w-4 stroke-[1.5]" />
{link.name} {link.name}
</span> </span>
</MenuItem> </Menu.Item>
</Link> </Link>
))} ))}
</div> </div>
<div className={`pt-2 ${isUserInstanceAdmin || false ? "pb-2" : ""}`}> <div className={`pt-2 ${isUserInstanceAdmin || false ? "pb-2" : ""}`}>
<MenuItem <Menu.Item
as="button" as="button"
type="button" type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80" className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
@ -302,20 +302,20 @@ export const WorkspaceSidebarDropdown = observer(() => {
> >
<LogOut className="h-4 w-4 stroke-[1.5]" /> <LogOut className="h-4 w-4 stroke-[1.5]" />
Sign out Sign out
</MenuItem> </Menu.Item>
</div> </div>
{isUserInstanceAdmin && ( {isUserInstanceAdmin && (
<div className="p-2 pb-0"> <div className="p-2 pb-0">
<Link href={GOD_MODE_URL}> <Link href={GOD_MODE_URL}>
<MenuItem as="button" type="button" className="w-full"> <Menu.Item as="button" type="button" className="w-full">
<span className="flex w-full items-center justify-center rounded bg-custom-primary-100/20 px-2 py-1 text-sm font-medium text-custom-primary-100 hover:bg-custom-primary-100/30 hover:text-custom-primary-200"> <span className="flex w-full items-center justify-center rounded bg-custom-primary-100/20 px-2 py-1 text-sm font-medium text-custom-primary-100 hover:bg-custom-primary-100/30 hover:text-custom-primary-200">
Enter God Mode Enter God Mode
</span> </span>
</MenuItem> </Menu.Item>
</Link> </Link>
</div> </div>
)} )}
</MenuItems> </Menu.Items>
</Transition> </Transition>
</Menu> </Menu>
)} )}

View File

@ -16,7 +16,7 @@
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.3.0", "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^1.3.0",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@blueprintjs/popover2": "^1.13.3", "@blueprintjs/popover2": "^1.13.3",
"@headlessui/react": "^2.0.4", "@headlessui/react": "^1.7.3",
"@nivo/bar": "0.80.0", "@nivo/bar": "0.80.0",
"@nivo/calendar": "0.80.0", "@nivo/calendar": "0.80.0",
"@nivo/core": "0.80.0", "@nivo/core": "0.80.0",

View File

@ -1592,7 +1592,7 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5"
integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw== integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==
"@headlessui/react@^1.7.13", "@headlessui/react@^1.7.19": "@headlessui/react@^1.7.13", "@headlessui/react@^1.7.19", "@headlessui/react@^1.7.3":
version "1.7.19" version "1.7.19"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.19.tgz#91c78cf5fcb254f4a0ebe96936d48421caf75f40" resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.19.tgz#91c78cf5fcb254f4a0ebe96936d48421caf75f40"
integrity sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw== integrity sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==
@ -1600,7 +1600,7 @@
"@tanstack/react-virtual" "^3.0.0-beta.60" "@tanstack/react-virtual" "^3.0.0-beta.60"
client-only "^0.0.1" client-only "^0.0.1"
"@headlessui/react@^2.0.3", "@headlessui/react@^2.0.4": "@headlessui/react@^2.0.3":
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-2.0.4.tgz#46cb39ca9dde3c2d15f4706c81dad78405b608f0" resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-2.0.4.tgz#46cb39ca9dde3c2d15f4706c81dad78405b608f0"
integrity sha512-16d/rOLeYsFsmPlRmXGu8DCBzrWD0zV1Ccx3n73wN87yFu8Y9+X04zflv8EJEt9TAYRyLKOmQXUnOnqQl6NgpA== integrity sha512-16d/rOLeYsFsmPlRmXGu8DCBzrWD0zV1Ccx3n73wN87yFu8Y9+X04zflv8EJEt9TAYRyLKOmQXUnOnqQl6NgpA==