From f2d576194fa005d3fbf43c06723befce2e88942a Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:11:20 +0530 Subject: [PATCH 01/11] chore: dynamic position state dropdown for issue view --- .../core/views/board-view/single-issue.tsx | 8 +- .../core/views/calendar-view/single-issue.tsx | 9 +- .../core/views/list-view/single-issue.tsx | 8 +- .../views/spreadsheet-view/single-issue.tsx | 12 +- web/components/issues/view-select/index.ts | 3 +- web/components/issues/view-select/state.tsx | 138 --------- web/components/states/index.ts | 1 + web/components/states/state-select.tsx | 263 ++++++++++++++++++ 8 files changed, 282 insertions(+), 160 deletions(-) delete mode 100644 web/components/issues/view-select/state.tsx create mode 100644 web/components/states/state-select.tsx diff --git a/web/components/core/views/board-view/single-issue.tsx b/web/components/core/views/board-view/single-issue.tsx index 455ba48e6ac..8a2cb2051f5 100644 --- a/web/components/core/views/board-view/single-issue.tsx +++ b/web/components/core/views/board-view/single-issue.tsx @@ -25,8 +25,8 @@ import { ViewIssueLabel, ViewPrioritySelect, ViewStartDateSelect, - ViewStateSelect, } from "components/issues"; +import { StateSelect } from "components/states"; // ui import { ContextMenu, CustomMenu, Tooltip } from "components/ui"; // icons @@ -321,12 +321,12 @@ export const SingleBoardIssue: React.FC = ({ /> )} {properties.state && ( - )} {properties.start_date && issue.start_date && ( diff --git a/web/components/core/views/calendar-view/single-issue.tsx b/web/components/core/views/calendar-view/single-issue.tsx index f6c1cc2f734..9b583a3aba6 100644 --- a/web/components/core/views/calendar-view/single-issue.tsx +++ b/web/components/core/views/calendar-view/single-issue.tsx @@ -22,8 +22,8 @@ import { ViewLabelSelect, ViewPrioritySelect, ViewStartDateSelect, - ViewStateSelect, } from "components/issues"; +import { StateSelect } from "components/states"; // icons import { LinkIcon, PaperClipIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; import { LayerDiagonalIcon } from "components/icons"; @@ -222,12 +222,11 @@ export const SingleCalendarIssue: React.FC = ({ /> )} {properties.state && ( - )} diff --git a/web/components/core/views/list-view/single-issue.tsx b/web/components/core/views/list-view/single-issue.tsx index d89ef69279b..8d7a7b592a1 100644 --- a/web/components/core/views/list-view/single-issue.tsx +++ b/web/components/core/views/list-view/single-issue.tsx @@ -17,8 +17,8 @@ import { ViewIssueLabel, ViewPrioritySelect, ViewStartDateSelect, - ViewStateSelect, } from "components/issues"; +import { StateSelect } from "components/states"; // ui import { Tooltip, CustomMenu, ContextMenu } from "components/ui"; // icons @@ -255,12 +255,12 @@ export const SingleListIssue: React.FC = ({ /> )} {properties.state && ( - )} {properties.start_date && issue.start_date && ( diff --git a/web/components/core/views/spreadsheet-view/single-issue.tsx b/web/components/core/views/spreadsheet-view/single-issue.tsx index 731d7f92175..b40e57d743b 100644 --- a/web/components/core/views/spreadsheet-view/single-issue.tsx +++ b/web/components/core/views/spreadsheet-view/single-issue.tsx @@ -12,8 +12,8 @@ import { ViewIssueLabel, ViewPrioritySelect, ViewStartDateSelect, - ViewStateSelect, } from "components/issues"; +import { StateSelect } from "components/states"; import { Popover2 } from "@blueprintjs/popover2"; // icons import { Icon } from "components/ui"; @@ -283,15 +283,13 @@ export const SingleSpreadsheetIssue: React.FC = ({ {properties.state && (
-
)} diff --git a/web/components/issues/view-select/index.ts b/web/components/issues/view-select/index.ts index d78a82ed35e..99191eb3d21 100644 --- a/web/components/issues/view-select/index.ts +++ b/web/components/issues/view-select/index.ts @@ -3,5 +3,4 @@ export * from "./due-date"; export * from "./estimate"; export * from "./label"; export * from "./priority"; -export * from "./start-date"; -export * from "./state"; +export * from "./start-date"; \ No newline at end of file diff --git a/web/components/issues/view-select/state.tsx b/web/components/issues/view-select/state.tsx deleted file mode 100644 index 08ca77d8086..00000000000 --- a/web/components/issues/view-select/state.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { useState } from "react"; - -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// services -import stateService from "services/state.service"; -import trackEventServices from "services/track-event.service"; -// ui -import { CustomSearchSelect, Tooltip } from "components/ui"; -// icons -import { StateGroupIcon } from "components/icons"; -// helpers -import { getStatesList } from "helpers/state.helper"; -// types -import { ICurrentUserResponse, IIssue } from "types"; -// fetch-keys -import { STATES_LIST } from "constants/fetch-keys"; - -type Props = { - issue: IIssue; - partialUpdateIssue: (formData: Partial, issue: IIssue) => void; - position?: "left" | "right"; - tooltipPosition?: "top" | "bottom"; - className?: string; - selfPositioned?: boolean; - customButton?: boolean; - user: ICurrentUserResponse | undefined; - isNotAllowed: boolean; -}; - -export const ViewStateSelect: React.FC = ({ - issue, - partialUpdateIssue, - position = "left", - tooltipPosition = "top", - className = "", - selfPositioned = false, - customButton = false, - user, - isNotAllowed, -}) => { - const [fetchStates, setFetchStates] = useState(false); - - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { data: stateGroups } = useSWR( - workspaceSlug && issue && fetchStates ? STATES_LIST(issue.project) : null, - workspaceSlug && issue && fetchStates - ? () => stateService.getStates(workspaceSlug as string, issue.project) - : null - ); - const states = getStatesList(stateGroups); - - const options = states?.map((state) => ({ - value: state.id, - query: state.name, - content: ( -
- - {state.name} -
- ), - })); - - const selectedOption = issue.state_detail; - - const stateLabel = ( - -
- - {selectedOption && ( - - )} - - {selectedOption?.name ?? "State"} -
-
- ); - - return ( - { - const oldState = states?.find((s) => s.id === issue.state); - const newState = states?.find((s) => s.id === data); - - partialUpdateIssue( - { - state: data, - state_detail: newState, - }, - issue - ); - trackEventServices.trackIssuePartialPropertyUpdateEvent( - { - workspaceSlug, - workspaceId: issue.workspace, - projectId: issue.project_detail.id, - projectIdentifier: issue.project_detail.identifier, - projectName: issue.project_detail.name, - issueId: issue.id, - }, - "ISSUE_PROPERTY_UPDATE_STATE", - user - ); - - if (oldState?.group !== "completed" && newState?.group !== "completed") { - trackEventServices.trackIssueMarkedAsDoneEvent( - { - workspaceSlug: issue.workspace_detail.slug, - workspaceId: issue.workspace_detail.id, - projectId: issue.project_detail.id, - projectIdentifier: issue.project_detail.identifier, - projectName: issue.project_detail.name, - issueId: issue.id, - }, - user - ); - } - }} - options={options} - {...(customButton ? { customButton: stateLabel } : { label: stateLabel })} - position={position} - disabled={isNotAllowed} - onOpen={() => setFetchStates(true)} - noChevron - selfPositioned={selfPositioned} - /> - ); -}; diff --git a/web/components/states/index.ts b/web/components/states/index.ts index 39285a77f5d..96c26eee34e 100644 --- a/web/components/states/index.ts +++ b/web/components/states/index.ts @@ -2,3 +2,4 @@ export * from "./create-update-state-inline"; export * from "./create-state-modal"; export * from "./delete-state-modal"; export * from "./single-state"; +export * from "./state-select"; diff --git a/web/components/states/state-select.tsx b/web/components/states/state-select.tsx new file mode 100644 index 00000000000..dc0988f1348 --- /dev/null +++ b/web/components/states/state-select.tsx @@ -0,0 +1,263 @@ +import React, { useRef, useState } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import stateService from "services/state.service"; +import trackEventServices from "services/track-event.service"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// headless ui +import { Combobox, Transition } from "@headlessui/react"; +// icons +import { ChevronDownIcon } from "@heroicons/react/20/solid"; +import { CheckIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { StateGroupIcon } from "components/icons"; +// types +import { Tooltip } from "components/ui"; +// constants +import { STATES_LIST } from "constants/fetch-keys"; +import { ICurrentUserResponse, IIssue } from "types"; +// helper +import { getStatesList } from "helpers/state.helper"; + +type Props = { + issue: IIssue; + partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + className?: string; + buttonClassName?: string; + optionsClassName?: string; + noBorder?: boolean; + noChevron?: boolean; + disabled?: boolean; + user: ICurrentUserResponse | undefined; +}; + +export const StateSelect: React.FC = ({ + issue, + partialUpdateIssue, + className = "", + buttonClassName = "", + optionsClassName = "", + noChevron = false, + noBorder = false, + disabled = false, + user, +}) => { + const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + const [fetchStates, setFetchStates] = useState(false); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const dropdownBtn = useRef(null); + const dropdownOptions = useRef(null); + + const { data: stateGroups } = useSWR( + workspaceSlug && issue && fetchStates ? STATES_LIST(issue.project) : null, + workspaceSlug && issue && fetchStates + ? () => stateService.getStates(workspaceSlug as string, issue.project) + : null + ); + const states = getStatesList(stateGroups); + + const options = states?.map((state) => ({ + value: state.id, + query: state.name, + content: ( +
+ + {state.name} +
+ ), + })); + + const filteredOptions = + query === "" + ? options + : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); + + const handleOnOpen = () => { + const dropdownOptionsRef = dropdownOptions.current; + const dropdownBtnRef = dropdownBtn.current; + + if (!dropdownOptionsRef || !dropdownBtnRef) return; + + const dropdownWidth = dropdownOptionsRef.clientWidth; + const dropdownHeight = dropdownOptionsRef.clientHeight; + + const dropdownBtnX = dropdownBtnRef.getBoundingClientRect().x; + const dropdownBtnY = dropdownBtnRef.getBoundingClientRect().y; + const dropdownBtnHeight = dropdownBtnRef.clientHeight; + + let top = dropdownBtnY + dropdownBtnHeight; + if (dropdownBtnY + dropdownHeight > window.innerHeight) + top = dropdownBtnY - dropdownHeight - 10; + else top = top + 10; + + let left = dropdownBtnX; + if (dropdownBtnX + dropdownWidth > window.innerWidth) left = dropdownBtnX - dropdownWidth; + + dropdownOptionsRef.style.top = `${Math.round(top)}px`; + dropdownOptionsRef.style.left = `${Math.round(left)}px`; + }; + + const selectedOption = issue.state_detail; + + const label = ( + +
+ + {selectedOption && ( + + )} + + {selectedOption?.name ?? "State"} +
+
+ ); + + useOutsideClickDetector(dropdownOptions, () => { + if (isOpen) setIsOpen(false); + }); + + return ( + { + const oldState = states?.find((s) => s.id === issue.state); + const newState = states?.find((s) => s.id === data); + + partialUpdateIssue( + { + state: data, + state_detail: newState, + }, + issue + ); + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_STATE", + user + ); + + if (oldState?.group !== "completed" && newState?.group !== "completed") { + trackEventServices.trackIssueMarkedAsDoneEvent( + { + workspaceSlug: issue.workspace_detail.slug, + workspaceId: issue.workspace_detail.id, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + user + ); + } + }} + disabled={disabled} + > + {({ open }: { open: boolean }) => { + if (open) { + handleOnOpen(); + setFetchStates(true); + } + + return ( + <> + + {label} + {!noChevron && !disabled && ( + + +
+ +
+ + setQuery(e.target.value)} + placeholder="Type to search..." + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${ + active || selected ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ active, selected }) => ( + <> + {option.content} + + + )} + + )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ + ); + }} +
+ ); +}; From fd7ddee323ddfb583885d925f6832ee3cca9769a Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:37:52 +0530 Subject: [PATCH 02/11] style: state select dropdown styling --- web/components/states/state-select.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/web/components/states/state-select.tsx b/web/components/states/state-select.tsx index dc0988f1348..944c61c2d9f 100644 --- a/web/components/states/state-select.tsx +++ b/web/components/states/state-select.tsx @@ -208,15 +208,15 @@ export const StateSelect: React.FC = ({
-
- +
+ setQuery(e.target.value)} - placeholder="Type to search..." + placeholder="Search" displayValue={(assigned: any) => assigned?.name} />
@@ -229,7 +229,7 @@ export const StateSelect: React.FC = ({ value={option.value} className={({ active, selected }) => `flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${ - active || selected ? "bg-custom-background-80" : "" + active && !selected ? "bg-custom-background-80" : "" } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } > @@ -237,7 +237,9 @@ export const StateSelect: React.FC = ({ <> {option.content} )} From 3474edb884f6cd470873744ad202f7e02ddcc366 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 11 Sep 2023 18:51:11 +0530 Subject: [PATCH 03/11] fix: state icon attribute names --- web/components/cycles/active-cycle-details.tsx | 2 +- web/components/cycles/cycles-view.tsx | 2 +- web/components/icons/module/cancelled.tsx | 2 +- web/components/icons/module/paused.tsx | 2 +- web/components/icons/state/backlog.tsx | 2 +- web/components/icons/state/cancelled.tsx | 2 +- web/components/icons/state/started.tsx | 4 ++-- web/components/icons/state/unstarted.tsx | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/web/components/cycles/active-cycle-details.tsx b/web/components/cycles/active-cycle-details.tsx index 062dd57e74c..7816f0edba1 100644 --- a/web/components/cycles/active-cycle-details.tsx +++ b/web/components/cycles/active-cycle-details.tsx @@ -127,7 +127,7 @@ export const ActiveCycleDetails: React.FC = () => { cy="34.375" r="22" stroke="rgb(var(--color-text-400))" - stroke-linecap="round" + strokeLinecap="round" /> = ({ cycles, mutateCycles, viewType }) cy="34.375" r="22" stroke="rgb(var(--color-text-400))" - stroke-linecap="round" + strokeLinecap="round" /> = ({ fill="none" xmlns="http://www.w3.org/2000/svg" > - + = ({ width = "20", height = "20", fill="none" xmlns="http://www.w3.org/2000/svg" > - + = ({ fill="none" xmlns="http://www.w3.org/2000/svg" > - + ); diff --git a/web/components/icons/state/cancelled.tsx b/web/components/icons/state/cancelled.tsx index 1c3c4e3d244..4b06d80ba88 100644 --- a/web/components/icons/state/cancelled.tsx +++ b/web/components/icons/state/cancelled.tsx @@ -19,7 +19,7 @@ export const StateGroupCancelledIcon: React.FC = ({ fill="none" xmlns="http://www.w3.org/2000/svg" > - + = ({ viewBox="0 0 12 12" fill="none" > - - + + ); diff --git a/web/components/icons/state/unstarted.tsx b/web/components/icons/state/unstarted.tsx index 61a782b1f75..aa0d44935e5 100644 --- a/web/components/icons/state/unstarted.tsx +++ b/web/components/icons/state/unstarted.tsx @@ -19,6 +19,6 @@ export const StateGroupUnstartedIcon: React.FC = ({ fill="none" xmlns="http://www.w3.org/2000/svg" > - + ); From 30522696737660993a0a367edf430c7bcb12acba Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 19 Sep 2023 14:34:18 +0530 Subject: [PATCH 04/11] chore: state select dynamic dropdown --- .../core/views/board-view/board-header.tsx | 2 - .../core/views/board-view/single-issue.tsx | 60 ++++++- .../core/views/calendar-view/single-issue.tsx | 50 +++++- .../core/views/list-view/single-issue.tsx | 51 +++++- .../views/spreadsheet-view/single-issue.tsx | 59 ++++++- web/components/project/index.ts | 1 + .../{states => project}/state-select.tsx | 155 ++++++------------ web/components/states/index.ts | 1 - web/helpers/dyanamic-dropdown-position.ts | 26 +++ 9 files changed, 270 insertions(+), 135 deletions(-) rename web/components/{states => project}/state-select.tsx (60%) create mode 100644 web/helpers/dyanamic-dropdown-position.ts diff --git a/web/components/core/views/board-view/board-header.tsx b/web/components/core/views/board-view/board-header.tsx index 1cbfdc81a98..9913ce73339 100644 --- a/web/components/core/views/board-view/board-header.tsx +++ b/web/components/core/views/board-view/board-header.tsx @@ -50,8 +50,6 @@ export const BoardHeader: React.FC = ({ const { displayFilters, groupedIssues } = viewProps; - console.log("dF", displayFilters); - const { data: issueLabels } = useSWR( workspaceSlug && projectId && displayFilters?.group_by === "labels" ? PROJECT_ISSUE_LABELS(projectId.toString()) diff --git a/web/components/core/views/board-view/single-issue.tsx b/web/components/core/views/board-view/single-issue.tsx index 8fbcdde31d2..eb163faa67c 100644 --- a/web/components/core/views/board-view/single-issue.tsx +++ b/web/components/core/views/board-view/single-issue.tsx @@ -13,6 +13,7 @@ import { } from "react-beautiful-dnd"; // services import issuesService from "services/issues.service"; +import trackEventServices from "services/track-event.service"; // hooks import useToast from "hooks/use-toast"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; @@ -25,7 +26,7 @@ import { ViewPrioritySelect, ViewStartDateSelect, } from "components/issues"; -import { StateSelect } from "components/states"; +import { StateSelect } from "components/project"; // ui import { ContextMenu, CustomMenu, Tooltip } from "components/ui"; // icons @@ -44,7 +45,14 @@ import { LayerDiagonalIcon } from "components/icons"; import { handleIssuesMutation } from "constants/issue"; import { copyTextToClipboard } from "helpers/string.helper"; // types -import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types"; +import { + ICurrentUserResponse, + IIssue, + IIssueViewProps, + IState, + ISubIssueResponse, + UserAuth, +} from "types"; // fetch-keys import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys"; @@ -188,6 +196,44 @@ export const SingleBoardIssue: React.FC = ({ }); }; + const handleStateChange = (data: string, states: IState[] | undefined) => { + const oldState = states?.find((s) => s.id === issue.state); + const newState = states?.find((s) => s.id === data); + + partialUpdateIssue( + { + state: data, + state_detail: newState, + }, + issue + ); + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_STATE", + user + ); + if (oldState?.group !== "completed" && newState?.group !== "completed") { + trackEventServices.trackIssueMarkedAsDoneEvent( + { + workspaceSlug: issue.workspace_detail.slug, + workspaceId: issue.workspace_detail.id, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + user + ); + } + }; + useEffect(() => { if (snapshot.isDragging) handleTrashBox(snapshot.isDragging); }, [snapshot, handleTrashBox]); @@ -343,13 +389,12 @@ export const SingleBoardIssue: React.FC = ({ )}
@@ -369,11 +414,10 @@ export const SingleBoardIssue: React.FC = ({ )} {properties.state && ( )} {properties.start_date && issue.start_date && ( diff --git a/web/components/core/views/calendar-view/single-issue.tsx b/web/components/core/views/calendar-view/single-issue.tsx index 06c6b341040..ab8702a10ba 100644 --- a/web/components/core/views/calendar-view/single-issue.tsx +++ b/web/components/core/views/calendar-view/single-issue.tsx @@ -8,6 +8,7 @@ import { mutate } from "swr"; import { DraggableProvided, DraggableStateSnapshot } from "react-beautiful-dnd"; // services import issuesService from "services/issues.service"; +import trackEventServices from "services/track-event.service"; // hooks import useCalendarIssuesView from "hooks/use-calendar-issues-view"; import useIssuesProperties from "hooks/use-issue-properties"; @@ -22,14 +23,14 @@ import { ViewPrioritySelect, ViewStartDateSelect, } from "components/issues"; -import { StateSelect } from "components/states"; +import { StateSelect } from "components/project"; // icons import { LinkIcon, PaperClipIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; import { LayerDiagonalIcon } from "components/icons"; // helper import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // type -import { ICurrentUserResponse, IIssue, ISubIssueResponse } from "types"; +import { ICurrentUserResponse, IIssue, IState, ISubIssueResponse } from "types"; // fetch-keys import { CYCLE_ISSUES_WITH_PARAMS, @@ -153,6 +154,44 @@ export const SingleCalendarIssue: React.FC = ({ }); }; + const handleStateChange = (data: string, states: IState[] | undefined) => { + const oldState = states?.find((s) => s.id === issue.state); + const newState = states?.find((s) => s.id === data); + + partialUpdateIssue( + { + state: data, + state_detail: newState, + }, + issue + ); + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_STATE", + user + ); + if (oldState?.group !== "completed" && newState?.group !== "completed") { + trackEventServices.trackIssueMarkedAsDoneEvent( + { + workspaceSlug: issue.workspace_detail.slug, + workspaceId: issue.workspace_detail.id, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + user + ); + } + }; + const displayProperties = properties ? Object.values(properties).some((value) => value === true) : false; @@ -235,11 +274,10 @@ export const SingleCalendarIssue: React.FC = ({ )} {properties.state && ( )} {properties.start_date && issue.start_date && ( diff --git a/web/components/core/views/list-view/single-issue.tsx b/web/components/core/views/list-view/single-issue.tsx index 001fa5f5d21..48db049b274 100644 --- a/web/components/core/views/list-view/single-issue.tsx +++ b/web/components/core/views/list-view/single-issue.tsx @@ -6,6 +6,7 @@ import { mutate } from "swr"; // services import issuesService from "services/issues.service"; +import trackEventServices from "services/track-event.service"; // hooks import useToast from "hooks/use-toast"; // components @@ -16,10 +17,8 @@ import { ViewIssueLabel, ViewPrioritySelect, ViewStartDateSelect, - ViewStateSelect, - CreateUpdateDraftIssueModal, } from "components/issues"; -import { StateSelect } from "components/states"; +import { StateSelect } from "components/project"; // ui import { Tooltip, CustomMenu, ContextMenu } from "components/ui"; // icons @@ -41,6 +40,7 @@ import { ICurrentUserResponse, IIssue, IIssueViewProps, + IState, ISubIssueResponse, IUserProfileProjectSegregation, UserAuth, @@ -182,6 +182,44 @@ export const SingleListIssue: React.FC = ({ }); }; + const handleStateChange = (data: string, states: IState[] | undefined) => { + const oldState = states?.find((s) => s.id === issue.state); + const newState = states?.find((s) => s.id === data); + + partialUpdateIssue( + { + state: data, + state_detail: newState, + }, + issue + ); + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_STATE", + user + ); + if (oldState?.group !== "completed" && newState?.group !== "completed") { + trackEventServices.trackIssueMarkedAsDoneEvent( + { + workspaceSlug: issue.workspace_detail.slug, + workspaceId: issue.workspace_detail.id, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + user + ); + } + }; + const issuePath = isArchivedIssues ? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}` : isDraftIssues @@ -301,11 +339,10 @@ export const SingleListIssue: React.FC = ({ )} {properties.state && ( )} {properties.start_date && issue.start_date && ( diff --git a/web/components/core/views/spreadsheet-view/single-issue.tsx b/web/components/core/views/spreadsheet-view/single-issue.tsx index b40e57d743b..3fcdbd37e42 100644 --- a/web/components/core/views/spreadsheet-view/single-issue.tsx +++ b/web/components/core/views/spreadsheet-view/single-issue.tsx @@ -13,7 +13,7 @@ import { ViewPrioritySelect, ViewStartDateSelect, } from "components/issues"; -import { StateSelect } from "components/states"; +import { StateSelect } from "components/project"; import { Popover2 } from "@blueprintjs/popover2"; // icons import { Icon } from "components/ui"; @@ -28,6 +28,7 @@ import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import useToast from "hooks/use-toast"; // services import issuesService from "services/issues.service"; +import trackEventServices from "services/track-event.service"; // constant import { CYCLE_DETAILS, @@ -39,7 +40,14 @@ import { VIEW_ISSUES, } from "constants/fetch-keys"; // types -import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types"; +import { + ICurrentUserResponse, + IIssue, + IState, + ISubIssueResponse, + Properties, + UserAuth, +} from "types"; // helper import { copyTextToClipboard } from "helpers/string.helper"; import { renderLongDetailDateFormat } from "helpers/date-time.helper"; @@ -180,6 +188,44 @@ export const SingleSpreadsheetIssue: React.FC = ({ }); }; + const handleStateChange = (data: string, states: IState[] | undefined) => { + const oldState = states?.find((s) => s.id === issue.state); + const newState = states?.find((s) => s.id === data); + + partialUpdateIssue( + { + state: data, + state_detail: newState, + }, + issue + ); + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_STATE", + user + ); + if (oldState?.group !== "completed" && newState?.group !== "completed") { + trackEventServices.trackIssueMarkedAsDoneEvent( + { + workspaceSlug: issue.workspace_detail.slug, + workspaceId: issue.workspace_detail.id, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + user + ); + } + }; + const paddingLeft = `${nestingLevel * 68}px`; const tooltipPosition = index === 0 ? "bottom" : "top"; @@ -284,12 +330,11 @@ export const SingleSpreadsheetIssue: React.FC = ({ {properties.state && (
)} diff --git a/web/components/project/index.ts b/web/components/project/index.ts index 23ad6ddbad1..dca5c42fe5d 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -7,3 +7,4 @@ export * from "./single-project-card"; export * from "./single-sidebar-project"; export * from "./confirm-project-leave-modal"; export * from "./member-select"; +export * from "./state-select"; diff --git a/web/components/states/state-select.tsx b/web/components/project/state-select.tsx similarity index 60% rename from web/components/states/state-select.tsx rename to web/components/project/state-select.tsx index 944c61c2d9f..2d3a6daec57 100644 --- a/web/components/states/state-select.tsx +++ b/web/components/project/state-select.tsx @@ -1,14 +1,13 @@ -import React, { useRef, useState } from "react"; +import React, { useEffect, useRef, useState, useCallback } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; -// services -import stateService from "services/state.service"; -import trackEventServices from "services/track-event.service"; // hooks import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// services +import stateService from "services/state.service"; // headless ui import { Combobox, Transition } from "@headlessui/react"; // icons @@ -18,50 +17,49 @@ import { StateGroupIcon } from "components/icons"; // types import { Tooltip } from "components/ui"; // constants +import { IState } from "types"; import { STATES_LIST } from "constants/fetch-keys"; -import { ICurrentUserResponse, IIssue } from "types"; // helper import { getStatesList } from "helpers/state.helper"; +import { handleDropdownPosition } from "helpers/dyanamic-dropdown-position"; type Props = { - issue: IIssue; - partialUpdateIssue: (formData: Partial, issue: IIssue) => void; + value: IState; + onChange: (data: any, states: IState[] | undefined) => void; className?: string; buttonClassName?: string; optionsClassName?: string; - noBorder?: boolean; - noChevron?: boolean; + hideDropdownArrow?: boolean; disabled?: boolean; - user: ICurrentUserResponse | undefined; }; export const StateSelect: React.FC = ({ - issue, - partialUpdateIssue, + value, + onChange, className = "", buttonClassName = "", optionsClassName = "", - noChevron = false, - noBorder = false, + hideDropdownArrow = false, disabled = false, - user, }) => { const [query, setQuery] = useState(""); const [isOpen, setIsOpen] = useState(false); - const [fetchStates, setFetchStates] = useState(false); - - const router = useRouter(); - const { workspaceSlug } = router.query; const dropdownBtn = useRef(null); const dropdownOptions = useRef(null); + const [fetchStates, setFetchStates] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + const { data: stateGroups } = useSWR( - workspaceSlug && issue && fetchStates ? STATES_LIST(issue.project) : null, - workspaceSlug && issue && fetchStates - ? () => stateService.getStates(workspaceSlug as string, issue.project) + workspaceSlug && projectId && fetchStates ? STATES_LIST(projectId as string) : null, + workspaceSlug && projectId && fetchStates + ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null ); + const states = getStatesList(stateGroups); const options = states?.map((state) => ({ @@ -80,46 +78,31 @@ export const StateSelect: React.FC = ({ ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); - const handleOnOpen = () => { - const dropdownOptionsRef = dropdownOptions.current; - const dropdownBtnRef = dropdownBtn.current; - - if (!dropdownOptionsRef || !dropdownBtnRef) return; - - const dropdownWidth = dropdownOptionsRef.clientWidth; - const dropdownHeight = dropdownOptionsRef.clientHeight; - - const dropdownBtnX = dropdownBtnRef.getBoundingClientRect().x; - const dropdownBtnY = dropdownBtnRef.getBoundingClientRect().y; - const dropdownBtnHeight = dropdownBtnRef.clientHeight; - - let top = dropdownBtnY + dropdownBtnHeight; - if (dropdownBtnY + dropdownHeight > window.innerHeight) - top = dropdownBtnY - dropdownHeight - 10; - else top = top + 10; - - let left = dropdownBtnX; - if (dropdownBtnX + dropdownWidth > window.innerWidth) left = dropdownBtnX - dropdownWidth; - - dropdownOptionsRef.style.top = `${Math.round(top)}px`; - dropdownOptionsRef.style.left = `${Math.round(left)}px`; - }; - - const selectedOption = issue.state_detail; - const label = ( - +
- {selectedOption && ( - - )} + {value && } - {selectedOption?.name ?? "State"} + {value?.name ?? "State"}
); + const handleResize = useCallback(() => { + if (isOpen) { + handleDropdownPosition(dropdownBtn, dropdownOptions); + } + }, [isOpen, dropdownBtn, dropdownOptions]); + + useEffect(() => { + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [isOpen, handleResize]); + useOutsideClickDetector(dropdownOptions, () => { if (isOpen) setIsOpen(false); }); @@ -128,50 +111,16 @@ export const StateSelect: React.FC = ({ { - const oldState = states?.find((s) => s.id === issue.state); - const newState = states?.find((s) => s.id === data); - - partialUpdateIssue( - { - state: data, - state_detail: newState, - }, - issue - ); - trackEventServices.trackIssuePartialPropertyUpdateEvent( - { - workspaceSlug, - workspaceId: issue.workspace, - projectId: issue.project_detail.id, - projectIdentifier: issue.project_detail.identifier, - projectName: issue.project_detail.name, - issueId: issue.id, - }, - "ISSUE_PROPERTY_UPDATE_STATE", - user - ); - - if (oldState?.group !== "completed" && newState?.group !== "completed") { - trackEventServices.trackIssueMarkedAsDoneEvent( - { - workspaceSlug: issue.workspace_detail.slug, - workspaceId: issue.workspace_detail.id, - projectId: issue.project_detail.id, - projectIdentifier: issue.project_detail.identifier, - projectName: issue.project_detail.name, - issueId: issue.id, - }, - user - ); - } + onChange(data, states); }} disabled={disabled} > {({ open }: { open: boolean }) => { if (open) { - handleOnOpen(); + setIsOpen(true); + handleDropdownPosition(dropdownBtn, dropdownOptions); setFetchStates(true); } @@ -180,18 +129,14 @@ export const StateSelect: React.FC = ({ {label} - {!noChevron && !disabled && ( + {!hideDropdownArrow && !disabled && ( @@ -233,14 +178,16 @@ export const StateSelect: React.FC = ({ } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } > - {({ active, selected }) => ( + {({ selected }) => ( <> {option.content} - + {selected && ( + + )} )} diff --git a/web/components/states/index.ts b/web/components/states/index.ts index 96c26eee34e..39285a77f5d 100644 --- a/web/components/states/index.ts +++ b/web/components/states/index.ts @@ -2,4 +2,3 @@ export * from "./create-update-state-inline"; export * from "./create-state-modal"; export * from "./delete-state-modal"; export * from "./single-state"; -export * from "./state-select"; diff --git a/web/helpers/dyanamic-dropdown-position.ts b/web/helpers/dyanamic-dropdown-position.ts new file mode 100644 index 00000000000..a8e7c088d3f --- /dev/null +++ b/web/helpers/dyanamic-dropdown-position.ts @@ -0,0 +1,26 @@ +export const handleDropdownPosition = ( + dropdownBtn: React.RefObject, + dropdownOptions: React.RefObject + ) => { + const dropdownOptionsRef = dropdownOptions.current; + const dropdownBtnRef = dropdownBtn.current; + + if (!dropdownOptionsRef || !dropdownBtnRef) return; + + const dropdownWidth = dropdownOptionsRef.clientWidth; + const dropdownHeight = dropdownOptionsRef.clientHeight + 12; + + const dropdownBtnX = dropdownBtnRef.getBoundingClientRect().x; + const dropdownBtnY = dropdownBtnRef.getBoundingClientRect().y; + const dropdownBtnHeight = dropdownBtnRef.clientHeight; + + let top = dropdownBtnY + dropdownBtnHeight; + if (dropdownBtnY + dropdownHeight > window.innerHeight) top = dropdownBtnY - dropdownHeight; + else top = top + 10; + + let left = dropdownBtnX; + if (dropdownBtnX + dropdownWidth > window.innerWidth) left = dropdownBtnX - dropdownWidth; + + dropdownOptionsRef.style.top = `${Math.round(top)}px`; + dropdownOptionsRef.style.left = `${Math.round(left)}px`; + }; From 74bc8e5b69c498b8f6d7a34b9f4e68fda0de582e Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 19 Sep 2023 15:21:42 +0530 Subject: [PATCH 05/11] chore: member select dynamic dropdown --- .../core/views/board-view/single-issue.tsx | 38 ++- .../core/views/calendar-view/single-issue.tsx | 37 ++- .../core/views/list-view/single-issue.tsx | 37 ++- .../views/spreadsheet-view/single-issue.tsx | 40 +++- web/components/project/index.ts | 1 + web/components/project/members-select.tsx | 221 ++++++++++++++++++ 6 files changed, 339 insertions(+), 35 deletions(-) create mode 100644 web/components/project/members-select.tsx diff --git a/web/components/core/views/board-view/single-issue.tsx b/web/components/core/views/board-view/single-issue.tsx index eb163faa67c..2cc8d021828 100644 --- a/web/components/core/views/board-view/single-issue.tsx +++ b/web/components/core/views/board-view/single-issue.tsx @@ -19,14 +19,13 @@ import useToast from "hooks/use-toast"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { - ViewAssigneeSelect, ViewDueDateSelect, ViewEstimateSelect, ViewIssueLabel, ViewPrioritySelect, ViewStartDateSelect, } from "components/issues"; -import { StateSelect } from "components/project"; +import { StateSelect, MembersSelect } from "components/project"; // ui import { ContextMenu, CustomMenu, Tooltip } from "components/ui"; // icons @@ -234,6 +233,28 @@ export const SingleBoardIssue: React.FC = ({ } }; + const handleAssigneeChange = (data: any) => { + const newData = issue.assignees ?? []; + + if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); + else newData.push(data); + + partialUpdateIssue({ assignees_list: data }, issue); + + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_ASSIGNEE", + user + ); + }; + useEffect(() => { if (snapshot.isDragging) handleTrashBox(snapshot.isDragging); }, [snapshot, handleTrashBox]); @@ -444,13 +465,12 @@ export const SingleBoardIssue: React.FC = ({ )} {properties.assignee && ( - )} {properties.estimate && issue.estimate_point !== null && ( diff --git a/web/components/core/views/calendar-view/single-issue.tsx b/web/components/core/views/calendar-view/single-issue.tsx index ab8702a10ba..eb143ac1660 100644 --- a/web/components/core/views/calendar-view/single-issue.tsx +++ b/web/components/core/views/calendar-view/single-issue.tsx @@ -16,14 +16,13 @@ import useToast from "hooks/use-toast"; // components import { CustomMenu, Tooltip } from "components/ui"; import { - ViewAssigneeSelect, ViewDueDateSelect, ViewEstimateSelect, ViewLabelSelect, ViewPrioritySelect, ViewStartDateSelect, } from "components/issues"; -import { StateSelect } from "components/project"; +import { MembersSelect, StateSelect } from "components/project"; // icons import { LinkIcon, PaperClipIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; import { LayerDiagonalIcon } from "components/icons"; @@ -192,6 +191,28 @@ export const SingleCalendarIssue: React.FC = ({ } }; + const handleAssigneeChange = (data: any) => { + const newData = issue.assignees ?? []; + + if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); + else newData.push(data); + + partialUpdateIssue({ assignees_list: data }, issue); + + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_ASSIGNEE", + user + ); + }; + const displayProperties = properties ? Object.values(properties).some((value) => value === true) : false; @@ -306,12 +327,12 @@ export const SingleCalendarIssue: React.FC = ({ /> )} {properties.assignee && ( - )} {properties.estimate && issue.estimate_point !== null && ( diff --git a/web/components/core/views/list-view/single-issue.tsx b/web/components/core/views/list-view/single-issue.tsx index 48db049b274..a2eed038bc7 100644 --- a/web/components/core/views/list-view/single-issue.tsx +++ b/web/components/core/views/list-view/single-issue.tsx @@ -11,14 +11,13 @@ import trackEventServices from "services/track-event.service"; import useToast from "hooks/use-toast"; // components import { - ViewAssigneeSelect, ViewDueDateSelect, ViewEstimateSelect, ViewIssueLabel, ViewPrioritySelect, ViewStartDateSelect, } from "components/issues"; -import { StateSelect } from "components/project"; +import { MembersSelect, StateSelect } from "components/project"; // ui import { Tooltip, CustomMenu, ContextMenu } from "components/ui"; // icons @@ -220,6 +219,28 @@ export const SingleListIssue: React.FC = ({ } }; + const handleAssigneeChange = (data: any) => { + const newData = issue.assignees ?? []; + + if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); + else newData.push(data); + + partialUpdateIssue({ assignees_list: data }, issue); + + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_ASSIGNEE", + user + ); + }; + const issuePath = isArchivedIssues ? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}` : isDraftIssues @@ -363,12 +384,12 @@ export const SingleListIssue: React.FC = ({ )} {properties.labels && } {properties.assignee && ( - )} {properties.estimate && issue.estimate_point !== null && ( diff --git a/web/components/core/views/spreadsheet-view/single-issue.tsx b/web/components/core/views/spreadsheet-view/single-issue.tsx index 3fcdbd37e42..3963ddeba60 100644 --- a/web/components/core/views/spreadsheet-view/single-issue.tsx +++ b/web/components/core/views/spreadsheet-view/single-issue.tsx @@ -6,14 +6,13 @@ import { mutate } from "swr"; // components import { - ViewAssigneeSelect, ViewDueDateSelect, ViewEstimateSelect, ViewIssueLabel, ViewPrioritySelect, ViewStartDateSelect, } from "components/issues"; -import { StateSelect } from "components/project"; +import { MembersSelect, StateSelect } from "components/project"; import { Popover2 } from "@blueprintjs/popover2"; // icons import { Icon } from "components/ui"; @@ -226,6 +225,28 @@ export const SingleSpreadsheetIssue: React.FC = ({ } }; + const handleAssigneeChange = (data: any) => { + const newData = issue.assignees ?? []; + + if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); + else newData.push(data); + + partialUpdateIssue({ assignees_list: data }, issue); + + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_ASSIGNEE", + user + ); + }; + const paddingLeft = `${nestingLevel * 68}px`; const tooltipPosition = index === 0 ? "bottom" : "top"; @@ -353,14 +374,13 @@ export const SingleSpreadsheetIssue: React.FC = ({ )} {properties.assignee && (
-
)} diff --git a/web/components/project/index.ts b/web/components/project/index.ts index dca5c42fe5d..e06ba47d976 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -8,3 +8,4 @@ export * from "./single-sidebar-project"; export * from "./confirm-project-leave-modal"; export * from "./member-select"; export * from "./state-select"; +export * from "./members-select"; diff --git a/web/components/project/members-select.tsx b/web/components/project/members-select.tsx new file mode 100644 index 00000000000..3c5bf9e478a --- /dev/null +++ b/web/components/project/members-select.tsx @@ -0,0 +1,221 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; + +import { useRouter } from "next/router"; + +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import useProjectMembers from "hooks/use-project-members"; +import useWorkspaceMembers from "hooks/use-workspace-members"; +// headless ui +import { Combobox, Transition } from "@headlessui/react"; +// icons +import { ChevronDownIcon } from "@heroicons/react/20/solid"; +import { CheckIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +// types +import { AssigneesList, Avatar, Icon, Tooltip } from "components/ui"; +import { IUser } from "types"; + +// helper +import { handleDropdownPosition } from "helpers/dyanamic-dropdown-position"; + +type Props = { + value: string | string[]; + onChange: (data: any) => void; + membersDetails: IUser[]; + renderWorkspaceMembers?: boolean; + className?: string; + buttonClassName?: string; + optionsClassName?: string; + hideDropdownArrow?: boolean; + disabled?: boolean; +}; + +export const MembersSelect: React.FC = ({ + value, + onChange, + membersDetails, + renderWorkspaceMembers = false, + className = "", + buttonClassName = "", + optionsClassName = "", + hideDropdownArrow = false, + disabled = false, +}) => { + const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + const [fetchStates, setFetchStates] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const dropdownBtn = useRef(null); + const dropdownOptions = useRef(null); + + const { members } = useProjectMembers( + workspaceSlug?.toString(), + projectId?.toString(), + fetchStates && !renderWorkspaceMembers + ); + + const { workspaceMembers } = useWorkspaceMembers( + workspaceSlug?.toString() ?? "", + fetchStates && renderWorkspaceMembers + ); + + const membersOptions = renderWorkspaceMembers ? workspaceMembers : members; + + const options = membersOptions?.map((member) => ({ + value: member.member.id, + query: member.member.display_name, + content: ( +
+ + {member.member.display_name} +
+ ), + })); + + const filteredOptions = + query === "" + ? options + : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); + + const label = ( + 0 + ? membersDetails.map((assignee) => assignee?.display_name).join(", ") + : "No Assignee" + } + position="top" + > +
+ {value && value.length > 0 && Array.isArray(value) ? ( + + ) : ( + + + + )} +
+
+ ); + + useOutsideClickDetector(dropdownOptions, () => { + if (isOpen) setIsOpen(false); + }); + + const handleResize = useCallback(() => { + if (isOpen) { + handleDropdownPosition(dropdownBtn, dropdownOptions); + } + }, [isOpen]); + + useEffect(() => { + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [handleResize, isOpen]); + + return ( + + {({ open }: { open: boolean }) => { + if (open) { + setIsOpen(true); + handleDropdownPosition(dropdownBtn, dropdownOptions); + setFetchStates(true); + } + + return ( + <> + + {label} + {!hideDropdownArrow && !disabled && ( + + +
+ +
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${ + active && !selected ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ + ); + }} +
+ ); +}; From d6c96d423a40e55fb9eaf3831852fdfd49f752fb Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:02:28 +0530 Subject: [PATCH 06/11] chore: label select dynamic dropdown --- .../core/views/board-view/single-issue.tsx | 20 +- .../core/views/calendar-view/single-issue.tsx | 23 +- .../core/views/list-view/single-issue.tsx | 23 +- .../views/spreadsheet-view/single-issue.tsx | 21 +- web/components/project/index.ts | 1 + web/components/project/label-select.tsx | 274 ++++++++++++++++++ 6 files changed, 346 insertions(+), 16 deletions(-) create mode 100644 web/components/project/label-select.tsx diff --git a/web/components/core/views/board-view/single-issue.tsx b/web/components/core/views/board-view/single-issue.tsx index 2cc8d021828..3c236b25688 100644 --- a/web/components/core/views/board-view/single-issue.tsx +++ b/web/components/core/views/board-view/single-issue.tsx @@ -21,11 +21,10 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; import { ViewDueDateSelect, ViewEstimateSelect, - ViewIssueLabel, ViewPrioritySelect, ViewStartDateSelect, } from "components/issues"; -import { StateSelect, MembersSelect } from "components/project"; +import { StateSelect, MembersSelect, LabelSelect } from "components/project"; // ui import { ContextMenu, CustomMenu, Tooltip } from "components/ui"; // icons @@ -255,6 +254,15 @@ export const SingleBoardIssue: React.FC = ({ ); }; + const handleLabelChange = (data: any) => { + const newData = issue.labels ?? []; + + if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); + else newData.push(data); + + partialUpdateIssue({ labels_list: newData }, issue); + }; + useEffect(() => { if (snapshot.isDragging) handleTrashBox(snapshot.isDragging); }, [snapshot, handleTrashBox]); @@ -462,7 +470,13 @@ export const SingleBoardIssue: React.FC = ({ /> )} {properties.labels && issue.labels.length > 0 && ( - + )} {properties.assignee && ( = ({ ); }; + const handleLabelChange = (data: any) => { + const newData = issue.labels ?? []; + + if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); + else newData.push(data); + + partialUpdateIssue({ labels_list: newData }, issue); + }; + const displayProperties = properties ? Object.values(properties).some((value) => value === true) : false; @@ -318,12 +326,13 @@ export const SingleCalendarIssue: React.FC = ({ /> )} {properties.labels && issue.labels.length > 0 && ( - )} {properties.assignee && ( diff --git a/web/components/core/views/list-view/single-issue.tsx b/web/components/core/views/list-view/single-issue.tsx index a2eed038bc7..01065e0871f 100644 --- a/web/components/core/views/list-view/single-issue.tsx +++ b/web/components/core/views/list-view/single-issue.tsx @@ -13,11 +13,10 @@ import useToast from "hooks/use-toast"; import { ViewDueDateSelect, ViewEstimateSelect, - ViewIssueLabel, ViewPrioritySelect, ViewStartDateSelect, } from "components/issues"; -import { MembersSelect, StateSelect } from "components/project"; +import { LabelSelect, MembersSelect, StateSelect } from "components/project"; // ui import { Tooltip, CustomMenu, ContextMenu } from "components/ui"; // icons @@ -241,6 +240,15 @@ export const SingleListIssue: React.FC = ({ ); }; + const handleLabelChange = (data: any) => { + const newData = issue.labels ?? []; + + if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); + else newData.push(data); + + partialUpdateIssue({ labels_list: newData }, issue); + }; + const issuePath = isArchivedIssues ? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}` : isDraftIssues @@ -382,7 +390,16 @@ export const SingleListIssue: React.FC = ({ isNotAllowed={isNotAllowed} /> )} - {properties.labels && } + {properties.labels && ( + + )} {properties.assignee && ( = ({ ); }; + const handleLabelChange = (data: any) => { + const newData = issue.labels ?? []; + + if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); + else newData.push(data); + + partialUpdateIssue({ labels_list: newData }, issue); + }; + const paddingLeft = `${nestingLevel * 68}px`; const tooltipPosition = index === 0 ? "bottom" : "top"; @@ -386,7 +394,14 @@ export const SingleSpreadsheetIssue: React.FC = ({ )} {properties.labels && (
- +
)} diff --git a/web/components/project/index.ts b/web/components/project/index.ts index e06ba47d976..d31cece8264 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -9,3 +9,4 @@ export * from "./confirm-project-leave-modal"; export * from "./member-select"; export * from "./state-select"; export * from "./members-select"; +export * from "./label-select"; diff --git a/web/components/project/label-select.tsx b/web/components/project/label-select.tsx new file mode 100644 index 00000000000..a209e318ed2 --- /dev/null +++ b/web/components/project/label-select.tsx @@ -0,0 +1,274 @@ +import React, { useEffect, useRef, useState, useCallback } from "react"; + +import useSWR from "swr"; + +import { useRouter } from "next/router"; + +// services +import issuesService from "services/issues.service"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// headless ui +import { Combobox, Transition } from "@headlessui/react"; +// component +import { CreateLabelModal } from "components/labels"; +// icons +import { ChevronDownIcon } from "@heroicons/react/20/solid"; +import { CheckIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { PlusIcon } from "lucide-react"; +// types +import { Tooltip } from "components/ui"; +import { ICurrentUserResponse, IIssueLabels } from "types"; +// constants +import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +// helper +import { handleDropdownPosition } from "helpers/dyanamic-dropdown-position"; + +type Props = { + value: any[]; + onChange: (data: any) => void; + className?: string; + buttonClassName?: string; + optionsClassName?: string; + maxRender?: number; + hideDropdownArrow?: boolean; + disabled?: boolean; + user: ICurrentUserResponse | undefined; +}; + +export const LabelSelect: React.FC = ({ + value, + onChange, + className = "", + buttonClassName = "", + optionsClassName = "", + maxRender = 2, + hideDropdownArrow = false, + disabled = false, + user, +}) => { + const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + const [fetchStates, setFetchStates] = useState(false); + + const [labelModal, setLabelModal] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const dropdownBtn = useRef(null); + const dropdownOptions = useRef(null); + + const { data: issueLabels } = useSWR( + projectId && fetchStates ? PROJECT_ISSUE_LABELS(projectId.toString()) : null, + workspaceSlug && projectId && fetchStates + ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) + : null + ); + + const options = issueLabels?.map((label) => ({ + value: label.id, + query: label.name, + content: ( +
+ + {label.name} +
+ ), + })); + + const filteredOptions = + query === "" + ? options + : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); + + const label = ( +
+ {value.length > 0 ? ( + value.length <= maxRender ? ( + <> + {value.map((label) => ( +
+
+ + {label.name} +
+
+ ))} + + ) : ( +
+ l.name).join(", ")} + > +
+ + {`${value.length} Labels`} +
+
+
+ ) + ) : ( + "" + )} +
+ ); + + const handleResize = useCallback(() => { + if (isOpen) { + handleDropdownPosition(dropdownBtn, dropdownOptions); + } + }, [isOpen, dropdownBtn, dropdownOptions]); + + useEffect(() => { + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [isOpen, handleResize]); + + useOutsideClickDetector(dropdownOptions, () => { + if (isOpen) setIsOpen(false); + }); + + const footerOption = ( + + ); + + return ( + <> + {projectId && ( + setLabelModal(false)} + projectId={projectId.toString()} + user={user} + /> + )} + + {({ open }: { open: boolean }) => { + if (open) { + setIsOpen(true); + handleDropdownPosition(dropdownBtn, dropdownOptions); + setFetchStates(true); + } + + return ( + <> + + {label} + {!hideDropdownArrow && !disabled && ( + + +
+ +
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${ + active && !selected ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + + + )} + + )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+ {footerOption} +
+
+
+ + ); + }} +
+ + ); +}; From 3f9076be5c34366cc18d3f3e65f623e4290e734d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:27:10 +0530 Subject: [PATCH 07/11] chore: priority select dynamic dropdown --- .../core/views/board-view/single-issue.tsx | 37 ++-- .../core/views/calendar-view/single-issue.tsx | 38 ++-- .../core/views/list-view/single-issue.tsx | 37 ++-- .../views/spreadsheet-view/single-issue.tsx | 40 ++-- web/components/project/index.ts | 1 + web/components/project/label-select.tsx | 7 +- web/components/project/priority-select.tsx | 203 ++++++++++++++++++ web/components/project/state-select.tsx | 8 +- 8 files changed, 304 insertions(+), 67 deletions(-) create mode 100644 web/components/project/priority-select.tsx diff --git a/web/components/core/views/board-view/single-issue.tsx b/web/components/core/views/board-view/single-issue.tsx index 3c236b25688..f709426c357 100644 --- a/web/components/core/views/board-view/single-issue.tsx +++ b/web/components/core/views/board-view/single-issue.tsx @@ -18,13 +18,8 @@ import trackEventServices from "services/track-event.service"; import useToast from "hooks/use-toast"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components -import { - ViewDueDateSelect, - ViewEstimateSelect, - ViewPrioritySelect, - ViewStartDateSelect, -} from "components/issues"; -import { StateSelect, MembersSelect, LabelSelect } from "components/project"; +import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues"; +import { StateSelect, MembersSelect, LabelSelect, PrioritySelect } from "components/project"; // ui import { ContextMenu, CustomMenu, Tooltip } from "components/ui"; // icons @@ -49,6 +44,7 @@ import { IIssueViewProps, IState, ISubIssueResponse, + TIssuePriorities, UserAuth, } from "types"; // fetch-keys @@ -263,6 +259,22 @@ export const SingleBoardIssue: React.FC = ({ partialUpdateIssue({ labels_list: newData }, issue); }; + const handlePriorityChange = (data: TIssuePriorities) => { + partialUpdateIssue({ priority: data }, issue); + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_PRIORITY", + user + ); + }; + useEffect(() => { if (snapshot.isDragging) handleTrashBox(snapshot.isDragging); }, [snapshot, handleTrashBox]); @@ -433,12 +445,11 @@ export const SingleBoardIssue: React.FC = ({ }`} > {properties.priority && ( - )} {properties.state && ( diff --git a/web/components/core/views/calendar-view/single-issue.tsx b/web/components/core/views/calendar-view/single-issue.tsx index 2c96231aff3..4f2d8ec33de 100644 --- a/web/components/core/views/calendar-view/single-issue.tsx +++ b/web/components/core/views/calendar-view/single-issue.tsx @@ -15,20 +15,15 @@ import useIssuesProperties from "hooks/use-issue-properties"; import useToast from "hooks/use-toast"; // components import { CustomMenu, Tooltip } from "components/ui"; -import { - ViewDueDateSelect, - ViewEstimateSelect, - ViewPrioritySelect, - ViewStartDateSelect, -} from "components/issues"; -import { LabelSelect, MembersSelect, StateSelect } from "components/project"; +import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues"; +import { LabelSelect, MembersSelect, PrioritySelect, StateSelect } from "components/project"; // icons import { LinkIcon, PaperClipIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; import { LayerDiagonalIcon } from "components/icons"; // helper import { copyTextToClipboard, truncateText } from "helpers/string.helper"; // type -import { ICurrentUserResponse, IIssue, IState, ISubIssueResponse } from "types"; +import { ICurrentUserResponse, IIssue, IState, ISubIssueResponse, TIssuePriorities } from "types"; // fetch-keys import { CYCLE_ISSUES_WITH_PARAMS, @@ -221,6 +216,22 @@ export const SingleCalendarIssue: React.FC = ({ partialUpdateIssue({ labels_list: newData }, issue); }; + const handlePriorityChange = (data: TIssuePriorities) => { + partialUpdateIssue({ priority: data }, issue); + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_PRIORITY", + user + ); + }; + const displayProperties = properties ? Object.values(properties).some((value) => value === true) : false; @@ -293,12 +304,11 @@ export const SingleCalendarIssue: React.FC = ({ {displayProperties && (
{properties.priority && ( - )} {properties.state && ( diff --git a/web/components/core/views/list-view/single-issue.tsx b/web/components/core/views/list-view/single-issue.tsx index 01065e0871f..1b8da522d59 100644 --- a/web/components/core/views/list-view/single-issue.tsx +++ b/web/components/core/views/list-view/single-issue.tsx @@ -10,13 +10,8 @@ import trackEventServices from "services/track-event.service"; // hooks import useToast from "hooks/use-toast"; // components -import { - ViewDueDateSelect, - ViewEstimateSelect, - ViewPrioritySelect, - ViewStartDateSelect, -} from "components/issues"; -import { LabelSelect, MembersSelect, StateSelect } from "components/project"; +import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues"; +import { LabelSelect, MembersSelect, PrioritySelect, StateSelect } from "components/project"; // ui import { Tooltip, CustomMenu, ContextMenu } from "components/ui"; // icons @@ -41,6 +36,7 @@ import { IState, ISubIssueResponse, IUserProfileProjectSegregation, + TIssuePriorities, UserAuth, } from "types"; // fetch-keys @@ -249,6 +245,22 @@ export const SingleListIssue: React.FC = ({ partialUpdateIssue({ labels_list: newData }, issue); }; + const handlePriorityChange = (data: TIssuePriorities) => { + partialUpdateIssue({ priority: data }, issue); + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_PRIORITY", + user + ); + }; + const issuePath = isArchivedIssues ? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}` : isDraftIssues @@ -358,12 +370,11 @@ export const SingleListIssue: React.FC = ({ }`} > {properties.priority && ( - )} {properties.state && ( diff --git a/web/components/core/views/spreadsheet-view/single-issue.tsx b/web/components/core/views/spreadsheet-view/single-issue.tsx index 67cff512284..4f499adb214 100644 --- a/web/components/core/views/spreadsheet-view/single-issue.tsx +++ b/web/components/core/views/spreadsheet-view/single-issue.tsx @@ -5,13 +5,8 @@ import { useRouter } from "next/router"; import { mutate } from "swr"; // components -import { - ViewDueDateSelect, - ViewEstimateSelect, - ViewPrioritySelect, - ViewStartDateSelect, -} from "components/issues"; -import { LabelSelect, MembersSelect, StateSelect } from "components/project"; +import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues"; +import { LabelSelect, MembersSelect, PrioritySelect, StateSelect } from "components/project"; import { Popover2 } from "@blueprintjs/popover2"; // icons import { Icon } from "components/ui"; @@ -44,6 +39,7 @@ import { IState, ISubIssueResponse, Properties, + TIssuePriorities, UserAuth, } from "types"; // helper @@ -224,6 +220,22 @@ export const SingleSpreadsheetIssue: React.FC = ({ } }; + const handlePriorityChange = (data: TIssuePriorities) => { + partialUpdateIssue({ priority: data }, issue); + trackEventServices.trackIssuePartialPropertyUpdateEvent( + { + workspaceSlug, + workspaceId: issue.workspace, + projectId: issue.project_detail.id, + projectIdentifier: issue.project_detail.identifier, + projectName: issue.project_detail.name, + issueId: issue.id, + }, + "ISSUE_PROPERTY_UPDATE_PRIORITY", + user + ); + }; + const handleAssigneeChange = (data: any) => { const newData = issue.assignees ?? []; @@ -369,14 +381,12 @@ export const SingleSpreadsheetIssue: React.FC = ({ )} {properties.priority && (
-
)} diff --git a/web/components/project/index.ts b/web/components/project/index.ts index d31cece8264..91c937ddc23 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -10,3 +10,4 @@ export * from "./member-select"; export * from "./state-select"; export * from "./members-select"; export * from "./label-select"; +export * from "./priority-select"; diff --git a/web/components/project/label-select.tsx b/web/components/project/label-select.tsx index a209e318ed2..492cf8d274a 100644 --- a/web/components/project/label-select.tsx +++ b/web/components/project/label-select.tsx @@ -176,6 +176,7 @@ export const LabelSelect: React.FC = ({ value={value} onChange={onChange} disabled={disabled} + multiple > {({ open }: { open: boolean }) => { if (open) { @@ -243,11 +244,7 @@ export const LabelSelect: React.FC = ({ {({ selected }) => ( <> {option.content} - + {selected && } )} diff --git a/web/components/project/priority-select.tsx b/web/components/project/priority-select.tsx new file mode 100644 index 00000000000..09b4e85afe9 --- /dev/null +++ b/web/components/project/priority-select.tsx @@ -0,0 +1,203 @@ +import React, { useEffect, useRef, useState, useCallback } from "react"; + +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// headless ui +import { Combobox, Transition } from "@headlessui/react"; +// icons +import { ChevronDownIcon } from "@heroicons/react/20/solid"; +import { CheckIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { PriorityIcon } from "components/icons"; +// components +import { Tooltip } from "components/ui"; +// types +import { TIssuePriorities } from "types"; +// constants +import { PRIORITIES } from "constants/project"; +// helper +import { handleDropdownPosition } from "helpers/dyanamic-dropdown-position"; + +type Props = { + value: TIssuePriorities; + onChange: (data: any) => void; + className?: string; + buttonClassName?: string; + optionsClassName?: string; + hideDropdownArrow?: boolean; + disabled?: boolean; +}; + +export const PrioritySelect: React.FC = ({ + value, + onChange, + className = "", + buttonClassName = "", + optionsClassName = "", + hideDropdownArrow = false, + disabled = false, +}) => { + const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + + const dropdownBtn = useRef(null); + const dropdownOptions = useRef(null); + + const options = PRIORITIES?.map((priority) => ({ + value: priority, + query: priority, + content: ( +
+ + {priority ?? "None"} +
+ ), + })); + + const filteredOptions = + query === "" + ? options + : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); + + const selectedOption = value ?? "None"; + + const label = ( + +
+ + + +
+
+ ); + + const handleResize = useCallback(() => { + if (isOpen) { + handleDropdownPosition(dropdownBtn, dropdownOptions); + } + }, [isOpen, dropdownBtn, dropdownOptions]); + + useEffect(() => { + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [isOpen, handleResize]); + + useOutsideClickDetector(dropdownOptions, () => { + if (isOpen) setIsOpen(false); + }); + + return ( + + {({ open }: { open: boolean }) => { + if (open) { + setIsOpen(true); + handleDropdownPosition(dropdownBtn, dropdownOptions); + } + + return ( + <> + + {label} + {!hideDropdownArrow && !disabled && ( + + +
+ +
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${ + active && !selected ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ + ); + }} +
+ ); +}; diff --git a/web/components/project/state-select.tsx b/web/components/project/state-select.tsx index 2d3a6daec57..9a2168f2dfb 100644 --- a/web/components/project/state-select.tsx +++ b/web/components/project/state-select.tsx @@ -181,13 +181,7 @@ export const StateSelect: React.FC = ({ {({ selected }) => ( <> {option.content} - {selected && ( - - )} + {selected && } )} From 3cbf8cf16a077bf806207fd17c5c068b8e486f54 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:40:14 +0530 Subject: [PATCH 08/11] chore: label select dropdown improvement --- .../core/views/board-view/single-issue.tsx | 10 +++------- .../core/views/calendar-view/single-issue.tsx | 10 +++------- web/components/core/views/list-view/single-issue.tsx | 10 +++------- .../core/views/spreadsheet-view/single-issue.tsx | 10 +++------- web/components/project/label-select.tsx | 12 +++++++----- 5 files changed, 19 insertions(+), 33 deletions(-) diff --git a/web/components/core/views/board-view/single-issue.tsx b/web/components/core/views/board-view/single-issue.tsx index f709426c357..6976c53e2be 100644 --- a/web/components/core/views/board-view/single-issue.tsx +++ b/web/components/core/views/board-view/single-issue.tsx @@ -251,12 +251,7 @@ export const SingleBoardIssue: React.FC = ({ }; const handleLabelChange = (data: any) => { - const newData = issue.labels ?? []; - - if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); - else newData.push(data); - - partialUpdateIssue({ labels_list: newData }, issue); + partialUpdateIssue({ labels_list: data }, issue); }; const handlePriorityChange = (data: TIssuePriorities) => { @@ -482,8 +477,9 @@ export const SingleBoardIssue: React.FC = ({ )} {properties.labels && issue.labels.length > 0 && ( = ({ }; const handleLabelChange = (data: any) => { - const newData = issue.labels ?? []; - - if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); - else newData.push(data); - - partialUpdateIssue({ labels_list: newData }, issue); + partialUpdateIssue({ labels_list: data }, issue); }; const handlePriorityChange = (data: TIssuePriorities) => { @@ -337,8 +332,9 @@ export const SingleCalendarIssue: React.FC = ({ )} {properties.labels && issue.labels.length > 0 && ( = ({ }; const handleLabelChange = (data: any) => { - const newData = issue.labels ?? []; - - if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); - else newData.push(data); - - partialUpdateIssue({ labels_list: newData }, issue); + partialUpdateIssue({ labels_list: data }, issue); }; const handlePriorityChange = (data: TIssuePriorities) => { @@ -403,8 +398,9 @@ export const SingleListIssue: React.FC = ({ )} {properties.labels && ( = ({ }; const handleLabelChange = (data: any) => { - const newData = issue.labels ?? []; - - if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); - else newData.push(data); - - partialUpdateIssue({ labels_list: newData }, issue); + partialUpdateIssue({ labels_list: data }, issue); }; const paddingLeft = `${nestingLevel * 68}px`; @@ -405,8 +400,9 @@ export const SingleSpreadsheetIssue: React.FC = ({ {properties.labels && (
void; + labelsDetails: any[]; className?: string; buttonClassName?: string; optionsClassName?: string; @@ -39,6 +40,7 @@ type Props = { export const LabelSelect: React.FC = ({ value, onChange, + labelsDetails, className = "", buttonClassName = "", optionsClassName = "", @@ -89,10 +91,10 @@ export const LabelSelect: React.FC = ({ const label = (
- {value.length > 0 ? ( - value.length <= maxRender ? ( + {labelsDetails.length > 0 ? ( + labelsDetails.length <= maxRender ? ( <> - {value.map((label) => ( + {labelsDetails.map((label) => (
= ({ l.name).join(", ")} + tooltipContent={labelsDetails.map((l) => l.name).join(", ")} >
From 5cbe41c407b123ef0bd4ff903e5130825bd3f9a1 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 19 Sep 2023 22:43:38 +0530 Subject: [PATCH 09/11] refactor: state dropdown location --- web/components/core/views/board-view/single-issue.tsx | 3 ++- web/components/core/views/calendar-view/single-issue.tsx | 3 ++- web/components/core/views/list-view/single-issue.tsx | 3 ++- web/components/core/views/spreadsheet-view/single-issue.tsx | 3 ++- web/components/project/index.ts | 1 - web/components/states/index.ts | 1 + web/components/{project => states}/state-select.tsx | 0 7 files changed, 9 insertions(+), 5 deletions(-) rename web/components/{project => states}/state-select.tsx (100%) diff --git a/web/components/core/views/board-view/single-issue.tsx b/web/components/core/views/board-view/single-issue.tsx index 6976c53e2be..0e27f895b1a 100644 --- a/web/components/core/views/board-view/single-issue.tsx +++ b/web/components/core/views/board-view/single-issue.tsx @@ -19,7 +19,8 @@ import useToast from "hooks/use-toast"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues"; -import { StateSelect, MembersSelect, LabelSelect, PrioritySelect } from "components/project"; +import { MembersSelect, LabelSelect, PrioritySelect } from "components/project"; +import { StateSelect } from "components/states"; // ui import { ContextMenu, CustomMenu, Tooltip } from "components/ui"; // icons diff --git a/web/components/core/views/calendar-view/single-issue.tsx b/web/components/core/views/calendar-view/single-issue.tsx index 33524d9a2c9..81d6f631f17 100644 --- a/web/components/core/views/calendar-view/single-issue.tsx +++ b/web/components/core/views/calendar-view/single-issue.tsx @@ -16,7 +16,8 @@ import useToast from "hooks/use-toast"; // components import { CustomMenu, Tooltip } from "components/ui"; import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues"; -import { LabelSelect, MembersSelect, PrioritySelect, StateSelect } from "components/project"; +import { LabelSelect, MembersSelect, PrioritySelect } from "components/project"; +import { StateSelect } from "components/states"; // icons import { LinkIcon, PaperClipIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; import { LayerDiagonalIcon } from "components/icons"; diff --git a/web/components/core/views/list-view/single-issue.tsx b/web/components/core/views/list-view/single-issue.tsx index db3d006e7a3..5c2eadf0a68 100644 --- a/web/components/core/views/list-view/single-issue.tsx +++ b/web/components/core/views/list-view/single-issue.tsx @@ -11,7 +11,8 @@ import trackEventServices from "services/track-event.service"; import useToast from "hooks/use-toast"; // components import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues"; -import { LabelSelect, MembersSelect, PrioritySelect, StateSelect } from "components/project"; +import { LabelSelect, MembersSelect, PrioritySelect } from "components/project"; +import { StateSelect } from "components/states"; // ui import { Tooltip, CustomMenu, ContextMenu } from "components/ui"; // icons diff --git a/web/components/core/views/spreadsheet-view/single-issue.tsx b/web/components/core/views/spreadsheet-view/single-issue.tsx index fe03481c24c..7a309f7288a 100644 --- a/web/components/core/views/spreadsheet-view/single-issue.tsx +++ b/web/components/core/views/spreadsheet-view/single-issue.tsx @@ -6,7 +6,8 @@ import { mutate } from "swr"; // components import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues"; -import { LabelSelect, MembersSelect, PrioritySelect, StateSelect } from "components/project"; +import { LabelSelect, MembersSelect, PrioritySelect } from "components/project"; +import { StateSelect } from "components/states"; import { Popover2 } from "@blueprintjs/popover2"; // icons import { Icon } from "components/ui"; diff --git a/web/components/project/index.ts b/web/components/project/index.ts index 91c937ddc23..329e826a82b 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -7,7 +7,6 @@ export * from "./single-project-card"; export * from "./single-sidebar-project"; export * from "./confirm-project-leave-modal"; export * from "./member-select"; -export * from "./state-select"; export * from "./members-select"; export * from "./label-select"; export * from "./priority-select"; diff --git a/web/components/states/index.ts b/web/components/states/index.ts index 39285a77f5d..96c26eee34e 100644 --- a/web/components/states/index.ts +++ b/web/components/states/index.ts @@ -2,3 +2,4 @@ export * from "./create-update-state-inline"; export * from "./create-state-modal"; export * from "./delete-state-modal"; export * from "./single-state"; +export * from "./state-select"; diff --git a/web/components/project/state-select.tsx b/web/components/states/state-select.tsx similarity index 100% rename from web/components/project/state-select.tsx rename to web/components/states/state-select.tsx From 0c63c693a83b1cdaaeb1e271bf6d28c142d55130 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 20 Sep 2023 01:08:21 +0530 Subject: [PATCH 10/11] chore: dropdown improvement and code refactor --- web/components/project/label-select.tsx | 136 ++++++++------------ web/components/project/members-select.tsx | 138 ++++++++------------- web/components/project/priority-select.tsx | 134 ++++++++------------ web/components/states/state-select.tsx | 133 ++++++++------------ web/helpers/dyanamic-dropdown-position.ts | 26 ---- web/hooks/use-dynamic-dropdown.tsx | 56 +++++++++ 6 files changed, 267 insertions(+), 356 deletions(-) delete mode 100644 web/helpers/dyanamic-dropdown-position.ts create mode 100644 web/hooks/use-dynamic-dropdown.tsx diff --git a/web/components/project/label-select.tsx b/web/components/project/label-select.tsx index a1da450c6cd..b4cc6da06ef 100644 --- a/web/components/project/label-select.tsx +++ b/web/components/project/label-select.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState, useCallback } from "react"; +import React, { useRef, useState } from "react"; import useSWR from "swr"; @@ -7,9 +7,9 @@ import { useRouter } from "next/router"; // services import issuesService from "services/issues.service"; // hooks -import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; // headless ui -import { Combobox, Transition } from "@headlessui/react"; +import { Combobox } from "@headlessui/react"; // component import { CreateLabelModal } from "components/labels"; // icons @@ -21,8 +21,6 @@ import { Tooltip } from "components/ui"; import { ICurrentUserResponse, IIssueLabels } from "types"; // constants import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; -// helper -import { handleDropdownPosition } from "helpers/dyanamic-dropdown-position"; type Props = { value: string[]; @@ -131,23 +129,7 @@ export const LabelSelect: React.FC = ({
); - const handleResize = useCallback(() => { - if (isOpen) { - handleDropdownPosition(dropdownBtn, dropdownOptions); - } - }, [isOpen, dropdownBtn, dropdownOptions]); - - useEffect(() => { - window.addEventListener("resize", handleResize); - - return () => { - window.removeEventListener("resize", handleResize); - }; - }, [isOpen, handleResize]); - - useOutsideClickDetector(dropdownOptions, () => { - if (isOpen) setIsOpen(false); - }); + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); const footerOption = (