mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: dynamic position dropdown (#2138)
* chore: dynamic position state dropdown for issue view * style: state select dropdown styling * fix: state icon attribute names * chore: state select dynamic dropdown * chore: member select dynamic dropdown * chore: label select dynamic dropdown * chore: priority select dynamic dropdown * chore: label select dropdown improvement * refactor: state dropdown location * chore: dropdown improvement and code refactor * chore: dynamic dropdown hook type added --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
parent
63c4792e70
commit
e01a0d20fe
@ -50,8 +50,6 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
console.log("dF", displayFilters);
|
||||
|
||||
const { data: issueLabels } = useSWR(
|
||||
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||
|
@ -13,19 +13,14 @@ 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";
|
||||
// components
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { MembersSelect, LabelSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
// ui
|
||||
import { ContextMenu, CustomMenu, Tooltip } from "components/ui";
|
||||
// icons
|
||||
@ -44,7 +39,15 @@ 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,
|
||||
TIssuePriorities,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
@ -188,6 +191,86 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
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 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 handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, 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]);
|
||||
@ -343,13 +426,12 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-left break-words line-clamp-2"
|
||||
onClick={() => {
|
||||
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
|
||||
else openPeekOverview();
|
||||
}}
|
||||
>
|
||||
{issue.name}
|
||||
<span className="text-sm text-left break-words line-clamp-2">{issue.name}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -359,21 +441,19 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
}`}
|
||||
>
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
selfPositioned
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
selfPositioned
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
@ -397,16 +477,22 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<ViewIssueLabel labelDetails={issue.label_details} maxRender={2} />
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
user={user}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
customButton
|
||||
user={user}
|
||||
selfPositioned
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
|
@ -8,28 +8,23 @@ 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";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CustomMenu, Tooltip } from "components/ui";
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewLabelSelect,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
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";
|
||||
// helper
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// type
|
||||
import { ICurrentUserResponse, IIssue, ISubIssueResponse } from "types";
|
||||
import { ICurrentUserResponse, IIssue, IState, ISubIssueResponse, TIssuePriorities } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
@ -153,6 +148,86 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
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 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 handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, 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;
|
||||
@ -225,22 +300,19 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
{displayProperties && (
|
||||
<div className="relative mt-1.5 w-full flex flex-wrap items-center gap-2 text-xs">
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
className="max-w-full"
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
@ -260,21 +332,23 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<ViewLabelSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
|
@ -6,19 +6,13 @@ 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
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
CreateUpdateDraftIssueModal,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
// ui
|
||||
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
|
||||
// icons
|
||||
@ -40,8 +34,10 @@ import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IIssueViewProps,
|
||||
IState,
|
||||
ISubIssueResponse,
|
||||
IUserProfileProjectSegregation,
|
||||
TIssuePriorities,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
@ -181,6 +177,86 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
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 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 handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, 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
|
||||
@ -290,21 +366,19 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
}`}
|
||||
>
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
@ -323,14 +397,24 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.labels && <ViewIssueLabel labelDetails={issue.label_details} maxRender={3} />}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
{properties.labels && (
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
maxRender={3}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
|
@ -5,15 +5,9 @@ import { useRouter } from "next/router";
|
||||
import { mutate } from "swr";
|
||||
|
||||
// components
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
import { Popover2 } from "@blueprintjs/popover2";
|
||||
// icons
|
||||
import { Icon } from "components/ui";
|
||||
@ -28,6 +22,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 +34,15 @@ import {
|
||||
VIEW_ISSUES,
|
||||
} from "constants/fetch-keys";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types";
|
||||
import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IState,
|
||||
ISubIssueResponse,
|
||||
Properties,
|
||||
TIssuePriorities,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// helper
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||
@ -180,6 +183,86 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
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 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 ?? [];
|
||||
|
||||
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 handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
const paddingLeft = `${nestingLevel * 68}px`;
|
||||
|
||||
const tooltipPosition = index === 0 ? "bottom" : "top";
|
||||
@ -283,47 +366,49 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
{properties.state && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
className="max-w-full"
|
||||
tooltipPosition={tooltipPosition}
|
||||
customButton
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.priority && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
tooltipPosition={tooltipPosition}
|
||||
noBorder
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
tooltipPosition={tooltipPosition}
|
||||
customButton
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.labels && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewIssueLabel labelDetails={issue.label_details} maxRender={1} />
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
user={user}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -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"
|
||||
/>
|
||||
<path
|
||||
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
|
||||
|
@ -221,7 +221,7 @@ export const CyclesView: React.FC<Props> = ({ cycles, mutateCycles, viewType })
|
||||
cy="34.375"
|
||||
r="22"
|
||||
stroke="rgb(var(--color-text-400))"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
|
||||
|
@ -20,7 +20,7 @@ export const ModuleCancelledIcon: React.FC<Props> = ({
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_4052_100277)">
|
||||
<g clipPath="url(#clip0_4052_100277)">
|
||||
<path
|
||||
d="M8 8.84L10.58 11.42C10.7 11.54 10.84 11.6 11 11.6C11.16 11.6 11.3 11.54 11.42 11.42C11.54 11.3 11.6 11.16 11.6 11C11.6 10.84 11.54 10.7 11.42 10.58L8.84 8L11.42 5.42C11.54 5.3 11.6 5.16 11.6 5C11.6 4.84 11.54 4.7 11.42 4.58C11.3 4.46 11.16 4.4 11 4.4C10.84 4.4 10.7 4.46 10.58 4.58L8 7.16L5.42 4.58C5.3 4.46 5.16 4.4 5 4.4C4.84 4.4 4.7 4.46 4.58 4.58C4.46 4.7 4.4 4.84 4.4 5C4.4 5.16 4.46 5.3 4.58 5.42L7.16 8L4.58 10.58C4.46 10.7 4.4 10.84 4.4 11C4.4 11.16 4.46 11.3 4.58 11.42C4.7 11.54 4.84 11.6 5 11.6C5.16 11.6 5.3 11.54 5.42 11.42L8 8.84ZM8 16C6.90667 16 5.87333 15.79 4.9 15.37C3.92667 14.95 3.07667 14.3767 2.35 13.65C1.62333 12.9233 1.05 12.0733 0.63 11.1C0.21 10.1267 0 9.09333 0 8C0 6.89333 0.21 5.85333 0.63 4.88C1.05 3.90667 1.62333 3.06 2.35 2.34C3.07667 1.62 3.92667 1.05 4.9 0.63C5.87333 0.21 6.90667 0 8 0C9.10667 0 10.1467 0.21 11.12 0.63C12.0933 1.05 12.94 1.62 13.66 2.34C14.38 3.06 14.95 3.90667 15.37 4.88C15.79 5.85333 16 6.89333 16 8C16 9.09333 15.79 10.1267 15.37 11.1C14.95 12.0733 14.38 12.9233 13.66 13.65C12.94 14.3767 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM8 14.8C9.89333 14.8 11.5 14.1367 12.82 12.81C14.14 11.4833 14.8 9.88 14.8 8C14.8 6.10667 14.14 4.5 12.82 3.18C11.5 1.86 9.89333 1.2 8 1.2C6.12 1.2 4.51667 1.86 3.19 3.18C1.86333 4.5 1.2 6.10667 1.2 8C1.2 9.88 1.86333 11.4833 3.19 12.81C4.51667 14.1367 6.12 14.8 8 14.8Z"
|
||||
fill="#ef4444"
|
||||
|
@ -16,7 +16,7 @@ export const ModulePausedIcon: React.FC<Props> = ({ width = "20", height = "20",
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_4052_100275)">
|
||||
<g clipPath="url(#clip0_4052_100275)">
|
||||
<path
|
||||
d="M6.4435 10.34C6.6145 10.34 6.75667 10.2825 6.87 10.1675C6.98333 10.0525 7.04 9.91 7.04 9.74V6.24C7.04 6.07 6.98217 5.9275 6.8665 5.8125C6.75082 5.6975 6.60749 5.64 6.4365 5.64C6.2655 5.64 6.12333 5.6975 6.01 5.8125C5.89667 5.9275 5.84 6.07 5.84 6.24V9.74C5.84 9.91 5.89783 10.0525 6.0135 10.1675C6.12918 10.2825 6.27251 10.34 6.4435 10.34ZM9.5635 10.34C9.7345 10.34 9.87667 10.2825 9.99 10.1675C10.1033 10.0525 10.16 9.91 10.16 9.74V6.24C10.16 6.07 10.1022 5.9275 9.9865 5.8125C9.87082 5.6975 9.72749 5.64 9.5565 5.64C9.3855 5.64 9.24333 5.6975 9.13 5.8125C9.01667 5.9275 8.96 6.07 8.96 6.24V9.74C8.96 9.91 9.01783 10.0525 9.1335 10.1675C9.24918 10.2825 9.39251 10.34 9.5635 10.34ZM8 16C6.89333 16 5.85333 15.79 4.88 15.37C3.90667 14.95 3.06 14.38 2.34 13.66C1.62 12.94 1.05 12.0933 0.63 11.12C0.21 10.1467 0 9.10667 0 8C0 7.54667 0.0366667 7.09993 0.11 6.6598C0.183333 6.21965 0.293333 5.78639 0.44 5.36C0.493333 5.21333 0.593333 5.11667 0.74 5.07C0.886667 5.02333 1.02667 5.04199 1.16 5.12596C1.30285 5.20993 1.40523 5.33327 1.46714 5.49596C1.52905 5.65865 1.54 5.82 1.5 5.98C1.42 6.31333 1.35 6.64765 1.29 6.98294C1.23 7.31823 1.2 7.65725 1.2 8C1.2 9.89833 1.85875 11.5063 3.17624 12.8238C4.49375 14.1413 6.10167 14.8 8 14.8C9.89833 14.8 11.5063 14.1413 12.8238 12.8238C14.1413 11.5063 14.8 9.89833 14.8 8C14.8 6.10167 14.1413 4.49375 12.8238 3.17624C11.5063 1.85875 9.89833 1.2 8 1.2C7.63235 1.2 7.26852 1.22667 6.90852 1.28C6.54852 1.33333 6.19235 1.41333 5.84 1.52C5.68 1.57333 5.52 1.56667 5.36 1.5C5.2 1.43333 5.08667 1.32667 5.02 1.18C4.95333 1.03333 4.96 0.886667 5.04 0.74C5.12 0.593333 5.23333 0.493333 5.38 0.44C5.79333 0.306667 6.21333 0.2 6.64 0.12C7.06667 0.04 7.49333 0 7.92 0C9.02667 0 10.07 0.21 11.05 0.63C12.03 1.05 12.8863 1.62 13.6189 2.34C14.3516 3.06 14.9316 3.90667 15.3589 4.88C15.7863 5.85333 16 6.89333 16 8C16 9.10667 15.79 10.1467 15.37 11.12C14.95 12.0933 14.38 12.94 13.66 13.66C12.94 14.38 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM2.65764 3.62C2.37921 3.62 2.14333 3.52255 1.95 3.32764C1.75667 3.13275 1.66 2.89608 1.66 2.61764C1.66 2.33921 1.75745 2.10333 1.95236 1.91C2.14725 1.71667 2.38392 1.62 2.66236 1.62C2.94079 1.62 3.17667 1.71745 3.37 1.91236C3.56333 2.10725 3.66 2.34392 3.66 2.62236C3.66 2.90079 3.56255 3.13667 3.36764 3.33C3.17275 3.52333 2.93608 3.62 2.65764 3.62Z"
|
||||
fill="#525252"
|
||||
|
@ -19,6 +19,6 @@ export const StateGroupBacklogIcon: React.FC<Props> = ({
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="6" cy="6" r="5.6" stroke={color} stroke-width="0.8" stroke-dasharray="4 4" />
|
||||
<circle cx="6" cy="6" r="5.6" stroke={color} strokeWidth="0.8" strokeDasharray="4 4" />
|
||||
</svg>
|
||||
);
|
||||
|
@ -19,7 +19,7 @@ export const StateGroupCancelledIcon: React.FC<Props> = ({
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_4052_100277)">
|
||||
<g clipPath="url(#clip0_4052_100277)">
|
||||
<path
|
||||
d="M8 8.84L10.58 11.42C10.7 11.54 10.84 11.6 11 11.6C11.16 11.6 11.3 11.54 11.42 11.42C11.54 11.3 11.6 11.16 11.6 11C11.6 10.84 11.54 10.7 11.42 10.58L8.84 8L11.42 5.42C11.54 5.3 11.6 5.16 11.6 5C11.6 4.84 11.54 4.7 11.42 4.58C11.3 4.46 11.16 4.4 11 4.4C10.84 4.4 10.7 4.46 10.58 4.58L8 7.16L5.42 4.58C5.3 4.46 5.16 4.4 5 4.4C4.84 4.4 4.7 4.46 4.58 4.58C4.46 4.7 4.4 4.84 4.4 5C4.4 5.16 4.46 5.3 4.58 5.42L7.16 8L4.58 10.58C4.46 10.7 4.4 10.84 4.4 11C4.4 11.16 4.46 11.3 4.58 11.42C4.7 11.54 4.84 11.6 5 11.6C5.16 11.6 5.3 11.54 5.42 11.42L8 8.84ZM8 16C6.90667 16 5.87333 15.79 4.9 15.37C3.92667 14.95 3.07667 14.3767 2.35 13.65C1.62333 12.9233 1.05 12.0733 0.63 11.1C0.21 10.1267 0 9.09333 0 8C0 6.89333 0.21 5.85333 0.63 4.88C1.05 3.90667 1.62333 3.06 2.35 2.34C3.07667 1.62 3.92667 1.05 4.9 0.63C5.87333 0.21 6.90667 0 8 0C9.10667 0 10.1467 0.21 11.12 0.63C12.0933 1.05 12.94 1.62 13.66 2.34C14.38 3.06 14.95 3.90667 15.37 4.88C15.79 5.85333 16 6.89333 16 8C16 9.09333 15.79 10.1267 15.37 11.1C14.95 12.0733 14.38 12.9233 13.66 13.65C12.94 14.3767 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM8 14.8C9.89333 14.8 11.5 14.1367 12.82 12.81C14.14 11.4833 14.8 9.88 14.8 8C14.8 6.10667 14.14 4.5 12.82 3.18C11.5 1.86 9.89333 1.2 8 1.2C6.12 1.2 4.51667 1.86 3.19 3.18C1.86333 4.5 1.2 6.10667 1.2 8C1.2 9.88 1.86333 11.4833 3.19 12.81C4.51667 14.1367 6.12 14.8 8 14.8Z"
|
||||
fill={color}
|
||||
|
@ -19,7 +19,7 @@ export const StateGroupStartedIcon: React.FC<Props> = ({
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
>
|
||||
<circle cx="6" cy="6" r="5.6" stroke={color} stroke-width="0.8" />
|
||||
<circle cx="6" cy="6" r="3.35" stroke={color} stroke-width="0.8" stroke-dasharray="2.4 2.4" />
|
||||
<circle cx="6" cy="6" r="5.6" stroke={color} strokeWidth="0.8" />
|
||||
<circle cx="6" cy="6" r="3.35" stroke={color} strokeWidth="0.8" strokeDasharray="2.4 2.4" />
|
||||
</svg>
|
||||
);
|
||||
|
@ -19,6 +19,6 @@ export const StateGroupUnstartedIcon: React.FC<Props> = ({
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="8" cy="8" r="7.4" stroke={color} stroke-width="1.2" />
|
||||
<circle cx="8" cy="8" r="7.4" stroke={color} strokeWidth="1.2" />
|
||||
</svg>
|
||||
);
|
||||
|
@ -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";
|
@ -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<IIssue>, 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<Props> = ({
|
||||
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: (
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
{state.name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const selectedOption = issue.state_detail;
|
||||
|
||||
const stateLabel = (
|
||||
<Tooltip
|
||||
tooltipHeading="State"
|
||||
tooltipContent={selectedOption?.name ?? ""}
|
||||
position={tooltipPosition}
|
||||
>
|
||||
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
|
||||
<span className="h-3.5 w-3.5">
|
||||
{selectedOption && (
|
||||
<StateGroupIcon stateGroup={selectedOption.group} color={selectedOption.color} />
|
||||
)}
|
||||
</span>
|
||||
<span className="truncate">{selectedOption?.name ?? "State"}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<CustomSearchSelect
|
||||
className={className}
|
||||
value={issue.state}
|
||||
onChange={(data: string) => {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -7,3 +7,6 @@ export * from "./single-project-card";
|
||||
export * from "./single-sidebar-project";
|
||||
export * from "./confirm-project-leave-modal";
|
||||
export * from "./member-select";
|
||||
export * from "./members-select";
|
||||
export * from "./label-select";
|
||||
export * from "./priority-select";
|
||||
|
243
web/components/project/label-select.tsx
Normal file
243
web/components/project/label-select.tsx
Normal file
@ -0,0 +1,243 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
// headless ui
|
||||
import { Combobox } 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";
|
||||
|
||||
type Props = {
|
||||
value: string[];
|
||||
onChange: (data: any) => void;
|
||||
labelsDetails: any[];
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
maxRender?: number;
|
||||
hideDropdownArrow?: boolean;
|
||||
disabled?: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const LabelSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
labelsDetails,
|
||||
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<any>(null);
|
||||
const dropdownOptions = useRef<any>(null);
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
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: (
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<span
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color,
|
||||
}}
|
||||
/>
|
||||
<span>{label.name}</span>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? options
|
||||
: options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const label = (
|
||||
<div className={`flex items-center gap-2 text-custom-text-200`}>
|
||||
{labelsDetails.length > 0 ? (
|
||||
labelsDetails.length <= maxRender ? (
|
||||
<>
|
||||
{labelsDetails.map((label) => (
|
||||
<div
|
||||
key={label.id}
|
||||
className="flex cursor-default items-center flex-shrink-0 rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label?.color ?? "#000000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex cursor-default items-center flex-shrink-0 rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm">
|
||||
<Tooltip
|
||||
position="top"
|
||||
tooltipHeading="Labels"
|
||||
tooltipContent={labelsDetails.map((l) => l.name).join(", ")}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
|
||||
{`${value.length} Labels`}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
const footerOption = (
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full select-none items-center rounded py-2 px-1 hover:bg-custom-background-80"
|
||||
onClick={() => setLabelModal(true)}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-1 text-custom-text-200">
|
||||
<PlusIcon className="h-4 w-4" aria-hidden="true" />
|
||||
<span>Create New Label</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectId && (
|
||||
<CreateLabelModal
|
||||
isOpen={labelModal}
|
||||
handleClose={() => setLabelModal(false)}
|
||||
projectId={projectId.toString()}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`flex-shrink-0 text-left ${className}`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
multiple
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open) {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
setFetchStates(true);
|
||||
} else if (isOpen) setIsOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Button
|
||||
ref={dropdownBtn}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: value.length <= maxRender
|
||||
? "cursor-pointer"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
|
||||
<Combobox.Options
|
||||
ref={dropdownOptions}
|
||||
className={`absolute z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap mt-1 ${optionsClassName}`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
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" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <CheckIcon className={`h-3.5 w-3.5`} />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
{footerOption}
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Combobox>
|
||||
</>
|
||||
);
|
||||
};
|
191
web/components/project/members-select.tsx
Normal file
191
web/components/project/members-select.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
import useProjectMembers from "hooks/use-project-members";
|
||||
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||
// headless ui
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// components
|
||||
import { AssigneesList, Avatar, Icon, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IUser } from "types";
|
||||
|
||||
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<Props> = ({
|
||||
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<any>(null);
|
||||
const dropdownOptions = useRef<any>(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: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? options
|
||||
: options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const label = (
|
||||
<Tooltip
|
||||
tooltipHeading="Assignee"
|
||||
tooltipContent={
|
||||
membersDetails.length > 0
|
||||
? membersDetails.map((assignee) => assignee?.display_name).join(", ")
|
||||
: "No Assignee"
|
||||
}
|
||||
position="top"
|
||||
>
|
||||
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
|
||||
{value && value.length > 0 && Array.isArray(value) ? (
|
||||
<AssigneesList userIds={value} length={3} showLength={true} />
|
||||
) : (
|
||||
<span
|
||||
className="flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none
|
||||
"
|
||||
>
|
||||
<Icon iconName="person" className="text-sm !leading-4" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`flex-shrink-0 text-left ${className}`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
multiple
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open) {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
setFetchStates(true);
|
||||
} else if (isOpen) setIsOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Button
|
||||
ref={dropdownBtn}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
|
||||
<Combobox.Options
|
||||
ref={dropdownOptions}
|
||||
className={`absolute z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap mt-1 ${optionsClassName}`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
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" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <CheckIcon className={`h-3.5 w-3.5`} />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
173
web/components/project/priority-select.tsx
Normal file
173
web/components/project/priority-select.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
// headless ui
|
||||
import { Combobox } 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";
|
||||
|
||||
type Props = {
|
||||
value: TIssuePriorities;
|
||||
onChange: (data: any) => void;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
hideDropdownArrow?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const PrioritySelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
optionsClassName = "",
|
||||
hideDropdownArrow = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const dropdownBtn = useRef<any>(null);
|
||||
const dropdownOptions = useRef<any>(null);
|
||||
|
||||
const options = PRIORITIES?.map((priority) => ({
|
||||
value: priority,
|
||||
query: priority,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<PriorityIcon priority={priority} className="text-sm" />
|
||||
{priority ?? "None"}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? options
|
||||
: options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const selectedOption = value ?? "None";
|
||||
|
||||
const label = (
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={selectedOption} position="top">
|
||||
<div
|
||||
className={`grid place-items-center rounded "h-6 w-6 border shadow-sm ${
|
||||
value === "urgent"
|
||||
? "border-red-500/20 bg-red-500"
|
||||
: "border-custom-border-300 bg-custom-background-100"
|
||||
} items-center`}
|
||||
>
|
||||
<span className="flex gap-1 items-center text-custom-text-200 text-xs">
|
||||
<PriorityIcon
|
||||
priority={value}
|
||||
className={`text-sm ${
|
||||
value === "urgent"
|
||||
? "text-white"
|
||||
: value === "high"
|
||||
? "text-orange-500"
|
||||
: value === "medium"
|
||||
? "text-yellow-500"
|
||||
: value === "low"
|
||||
? "text-green-500"
|
||||
: "text-custom-text-200"
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`flex-shrink-0 text-left ${className}`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open) {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
} else if (isOpen) setIsOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Button
|
||||
ref={dropdownBtn}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
|
||||
<Combobox.Options
|
||||
ref={dropdownOptions}
|
||||
className={`absolute z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap mt-1 ${optionsClassName}`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
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" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <CheckIcon className={`h-3.5 w-3.5`} />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
@ -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";
|
||||
|
177
web/components/states/state-select.tsx
Normal file
177
web/components/states/state-select.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
// headless ui
|
||||
import { Combobox } 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 { IState } from "types";
|
||||
import { STATES_LIST } from "constants/fetch-keys";
|
||||
// helper
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
|
||||
type Props = {
|
||||
value: IState;
|
||||
onChange: (data: any, states: IState[] | undefined) => void;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
hideDropdownArrow?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const StateSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
optionsClassName = "",
|
||||
hideDropdownArrow = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const dropdownBtn = useRef<any>(null);
|
||||
const dropdownOptions = useRef<any>(null);
|
||||
|
||||
const [fetchStates, setFetchStates] = useState<boolean>(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
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) => ({
|
||||
value: state.id,
|
||||
query: state.name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
{state.name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? options
|
||||
: options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const label = (
|
||||
<Tooltip tooltipHeading="State" tooltipContent={value?.name ?? ""} position="top">
|
||||
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
|
||||
<span className="h-3.5 w-3.5">
|
||||
{value && <StateGroupIcon stateGroup={value.group} color={value.color} />}
|
||||
</span>
|
||||
<span className="truncate">{value?.name ?? "State"}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`flex-shrink-0 text-left ${className}`}
|
||||
value={value.id}
|
||||
onChange={(data: string) => {
|
||||
onChange(data, states);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open) {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
setFetchStates(true);
|
||||
} else if (isOpen) setIsOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Button
|
||||
ref={dropdownBtn}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
|
||||
<Combobox.Options
|
||||
ref={dropdownOptions}
|
||||
className={`absolute z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap mt-1 ${optionsClassName}`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
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" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <CheckIcon className={`h-3.5 w-3.5`} />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
64
web/hooks/use-dynamic-dropdown.tsx
Normal file
64
web/hooks/use-dynamic-dropdown.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
|
||||
// hook
|
||||
import useOutsideClickDetector from "./use-outside-click-detector";
|
||||
|
||||
/**
|
||||
* Custom hook for dynamic dropdown position calculation.
|
||||
* @param isOpen - Indicates whether the dropdown is open.
|
||||
* @param handleClose - Callback to handle closing the dropdown.
|
||||
* @param buttonRef - Ref object for the button triggering the dropdown.
|
||||
* @param dropdownRef - Ref object for the dropdown element.
|
||||
*/
|
||||
|
||||
const useDynamicDropdownPosition = (
|
||||
isOpen: boolean,
|
||||
handleClose: () => void,
|
||||
buttonRef: React.RefObject<any>,
|
||||
dropdownRef: React.RefObject<any>
|
||||
) => {
|
||||
const handlePosition = useCallback(() => {
|
||||
const button = buttonRef.current;
|
||||
const dropdown = dropdownRef.current;
|
||||
|
||||
if (!dropdown || !button) return;
|
||||
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const dropdownRect = dropdown.getBoundingClientRect();
|
||||
|
||||
const { innerHeight, innerWidth, scrollX, scrollY } = window;
|
||||
|
||||
let top: number = buttonRect.bottom + scrollY;
|
||||
if (top + dropdownRect.height > innerHeight) top = innerHeight - dropdownRect.height;
|
||||
|
||||
let left: number = buttonRect.left + scrollX + (buttonRect.width - dropdownRect.width) / 2;
|
||||
if (left + dropdownRect.width > innerWidth) left = innerWidth - dropdownRect.width;
|
||||
|
||||
dropdown.style.top = `${Math.max(top, 5)}px`;
|
||||
dropdown.style.left = `${Math.max(left, 5)}px`;
|
||||
}, [buttonRef, dropdownRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) handlePosition();
|
||||
}, [handlePosition, isOpen]);
|
||||
|
||||
useOutsideClickDetector(dropdownRef, () => {
|
||||
if (isOpen) handleClose();
|
||||
});
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
if (isOpen) {
|
||||
handlePosition();
|
||||
}
|
||||
}, [handlePosition, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [isOpen, handleResize]);
|
||||
};
|
||||
|
||||
export default useDynamicDropdownPosition;
|
Loading…
Reference in New Issue
Block a user