chore: issue properties for list and kanban layouts and implemented estimates in project store (#2363)

* chore: issue properties for state, priorit, labels and members

* feat: implemented assignee, labels properties

* fix: implemented estimates in project store and issue properties

* chore: staer_date and due_date and validation properties in kanban
This commit is contained in:
guru_sainath 2023-10-04 14:38:49 +05:30 committed by GitHub
parent 7be038ac5a
commit b5b809500d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 2074 additions and 393 deletions

View File

@ -19,32 +19,39 @@ export const AllViews: React.FC = observer(() => {
const { issue: issueStore, project: projectStore, issueFilter: issueFilterStore } = useMobxStore(); const { issue: issueStore, project: projectStore, issueFilter: issueFilterStore } = useMobxStore();
useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES` : null, async () => { useSWR(
if (workspaceSlug && projectId) { workspaceSlug && projectId ? `PROJECT_ISSUES` : null,
await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId); async () => {
if (workspaceSlug && projectId) {
await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId);
await projectStore.fetchProjectStates(workspaceSlug, projectId); await projectStore.fetchProjectStates(workspaceSlug, projectId);
await projectStore.fetchProjectLabels(workspaceSlug, projectId); await projectStore.fetchProjectLabels(workspaceSlug, projectId);
await projectStore.fetchProjectMembers(workspaceSlug, projectId); await projectStore.fetchProjectMembers(workspaceSlug, projectId);
await projectStore.fetchProjectEstimates(workspaceSlug, projectId);
await issueStore.fetchIssues(workspaceSlug, projectId); await issueStore.fetchIssues(workspaceSlug, projectId);
} }
}); },
{ revalidateOnFocus: false }
);
const activeLayout = issueFilterStore.userDisplayFilters.layout; const activeLayout = issueFilterStore.userDisplayFilters.layout;
return ( return (
<div className="relative w-full h-full flex flex-col overflow-auto"> <div className="relative w-full h-full flex flex-col overflow-auto">
<AppliedFiltersRoot /> <AppliedFiltersList />
{activeLayout === "kanban" ? ( <div className="w-full h-full">
<KanBanLayout /> {activeLayout === "kanban" ? (
) : activeLayout === "calendar" ? ( <KanBanLayout />
<CalendarLayout /> ) : activeLayout === "calendar" ? (
) : activeLayout === "gantt_chart" ? ( <CalendarLayout />
<GanttLayout /> ) : activeLayout === "gantt_chart" ? (
) : activeLayout === "spreadsheet" ? ( <GanttLayout />
<SpreadsheetLayout /> ) : activeLayout === "spreadsheet" ? (
) : null} <SpreadsheetLayout />
) : null}
</div>
</div> </div>
); );
}); });

View File

@ -8,9 +8,18 @@ interface IssueBlockProps {
columnId: string; columnId: string;
issues: any; issues: any;
isDragDisabled: boolean; isDragDisabled: boolean;
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void;
display_properties: any;
} }
export const IssueBlock = ({ sub_group_id, columnId, issues, isDragDisabled }: IssueBlockProps) => ( export const IssueBlock = ({
sub_group_id,
columnId,
issues,
isDragDisabled,
handleIssues,
display_properties,
}: IssueBlockProps) => (
<> <>
{issues && issues.length > 0 ? ( {issues && issues.length > 0 ? (
<> <>
@ -30,14 +39,22 @@ export const IssueBlock = ({ sub_group_id, columnId, issues, isDragDisabled }: I
ref={provided.innerRef} ref={provided.innerRef}
> >
<div <div
className={`min-h-[106px] text-sm rounded p-2 px-3 shadow-custom-shadow-2xs space-y-[8px] border transition-all bg-custom-background-100 hover:cursor-grab ${ className={`text-sm rounded p-2 px-3 shadow-custom-shadow-2xs space-y-[8px] border transition-all bg-custom-background-100 hover:cursor-grab ${
snapshot.isDragging ? `border-custom-primary-100` : `border-transparent` snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`
}`} }`}
> >
<div className="text-xs line-clamp-1 text-custom-text-300">ONE-{issue.sequence_id}</div> {display_properties && display_properties?.key && (
<div className="text-xs line-clamp-1 text-custom-text-300">ONE-{issue.sequence_id}</div>
)}
<div className="line-clamp-2 h-[40px] text-sm font-medium text-custom-text-100">{issue.name}</div> <div className="line-clamp-2 h-[40px] text-sm font-medium text-custom-text-100">{issue.name}</div>
<div className="min-h-[22px]"> <div>
<KanBanProperties /> <KanBanProperties
sub_group_id={sub_group_id}
columnId={columnId}
issue={issue}
handleIssues={handleIssues}
display_properties={display_properties}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -19,12 +19,23 @@ export interface IGroupByKanBan {
sub_group_id: string; sub_group_id: string;
list: any; list: any;
listKey: string; listKey: string;
handleIssues?: () => void;
isDragDisabled: boolean; isDragDisabled: boolean;
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void;
display_properties: any;
} }
const GroupByKanBan: React.FC<IGroupByKanBan> = observer( const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
({ issues, sub_group_by, group_by, sub_group_id = "null", list, listKey, isDragDisabled }) => { ({
issues,
sub_group_by,
group_by,
sub_group_id = "null",
list,
listKey,
isDragDisabled,
handleIssues,
display_properties,
}) => {
const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
const verticalAlignPosition = (_list: any) => const verticalAlignPosition = (_list: any) =>
@ -42,7 +53,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
column_id={getValueFromObject(_list, listKey) as string} column_id={getValueFromObject(_list, listKey) as string}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
issues_count={issues?.[getValueFromObject(_list, listKey) as string].length || 0} issues_count={issues?.[getValueFromObject(_list, listKey) as string]?.length || 0}
/> />
</div> </div>
)} )}
@ -64,6 +75,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
columnId={getValueFromObject(_list, listKey) as string} columnId={getValueFromObject(_list, listKey) as string}
issues={issues[getValueFromObject(_list, listKey) as string]} issues={issues[getValueFromObject(_list, listKey) as string]}
isDragDisabled={isDragDisabled} isDragDisabled={isDragDisabled}
handleIssues={handleIssues}
display_properties={display_properties}
/> />
) : ( ) : (
isDragDisabled && ( isDragDisabled && (
@ -90,86 +103,101 @@ export interface IKanBan {
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
sub_group_id?: string; sub_group_id?: string;
handleIssues?: () => void;
handleDragDrop?: (result: any) => void | undefined; handleDragDrop?: (result: any) => void | undefined;
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void;
display_properties: any;
} }
export const KanBan: React.FC<IKanBan> = observer(({ issues, sub_group_by, group_by, sub_group_id = "null" }) => { export const KanBan: React.FC<IKanBan> = observer(
const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); ({ issues, sub_group_by, group_by, sub_group_id = "null", handleIssues, display_properties }) => {
const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
return ( return (
<div className="relative w-full h-full"> <div className="relative w-full h-full">
{group_by && group_by === "state" && ( {group_by && group_by === "state" && (
<GroupByKanBan <GroupByKanBan
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
list={projectStore?.projectStates} list={projectStore?.projectStates}
listKey={`id`} listKey={`id`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
/> handleIssues={handleIssues}
)} display_properties={display_properties}
/>
)}
{group_by && group_by === "state_detail.group" && ( {group_by && group_by === "state_detail.group" && (
<GroupByKanBan <GroupByKanBan
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
list={ISSUE_STATE_GROUPS} list={ISSUE_STATE_GROUPS}
listKey={`key`} listKey={`key`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
/> handleIssues={handleIssues}
)} display_properties={display_properties}
/>
)}
{group_by && group_by === "priority" && ( {group_by && group_by === "priority" && (
<GroupByKanBan <GroupByKanBan
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
list={ISSUE_PRIORITIES} list={ISSUE_PRIORITIES}
listKey={`key`} listKey={`key`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
/> handleIssues={handleIssues}
)} display_properties={display_properties}
/>
)}
{group_by && group_by === "labels" && ( {group_by && group_by === "labels" && (
<GroupByKanBan <GroupByKanBan
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
list={projectStore?.projectLabels} list={projectStore?.projectLabels}
listKey={`id`} listKey={`id`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
/> handleIssues={handleIssues}
)} display_properties={display_properties}
/>
)}
{group_by && group_by === "assignees" && ( {group_by && group_by === "assignees" && (
<GroupByKanBan <GroupByKanBan
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
list={projectStore?.projectMembers} list={projectStore?.projectMembers}
listKey={`member.id`} listKey={`member.id`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
/> handleIssues={handleIssues}
)} display_properties={display_properties}
/>
)}
{group_by && group_by === "created_by" && ( {group_by && group_by === "created_by" && (
<GroupByKanBan <GroupByKanBan
issues={issues} issues={issues}
group_by={group_by} group_by={group_by}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
list={projectStore?.projectMembers} list={projectStore?.projectMembers}
listKey={`member.id`} listKey={`member.id`}
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop} isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
/> handleIssues={handleIssues}
)} display_properties={display_properties}
</div> />
); )}
}); </div>
);
}
);

View File

@ -8,6 +8,7 @@ import { HeaderSubGroupByCard } from "./sub-group-by-card";
// constants // constants
import { issuePriorityByKey } from "constants/issue"; import { issuePriorityByKey } from "constants/issue";
export interface IPriorityHeader { export interface IPriorityHeader {
column_id: string; column_id: string;
sub_group_by: string | null; sub_group_by: string | null;

View File

@ -1,91 +1,205 @@
// mobx
import { observer } from "mobx-react-lite";
// lucide icons // lucide icons
import { Circle } from "lucide-react"; import { Layers, Link, Paperclip } from "lucide-react";
// components
import { IssuePropertyState } from "../properties/state";
import { IssuePropertyPriority } from "../properties/priority";
import { IssuePropertyLabels } from "../properties/labels";
import { IssuePropertyAssignee } from "../properties/assignee";
import { IssuePropertyEstimates } from "../properties/estimates";
import { IssuePropertyStartDate } from "../properties/date";
import { Tooltip } from "components/ui";
export const KanBanProperties = () => { export interface IKanBanProperties {
console.log("properties"); sub_group_id: string;
return ( columnId: string;
<div className="relative flex gap-2 overflow-hidden overflow-x-auto whitespace-nowrap"> issue: any;
{/* basic properties */} handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void;
{/* state */} display_properties: any;
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center"> }
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Circle width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">state</div>
</div>
{/* priority */} export const KanBanProperties: React.FC<IKanBanProperties> = observer(
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center"> ({ sub_group_id, columnId: group_id, issue, handleIssues, display_properties }) => {
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center"> const handleState = (id: string) => {
<Circle width={10} strokeWidth={2} /> if (handleIssues)
</div> handleIssues(
<div className="pl-0.5 pr-1 text-xs">priority</div> !sub_group_id && sub_group_id === "null" ? null : sub_group_id,
</div> !group_id && group_id === "null" ? null : group_id,
{ ...issue, state: id }
);
};
{/* label */} const handlePriority = (id: string) => {
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center"> if (handleIssues)
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center"> handleIssues(
<Circle width={10} strokeWidth={2} /> !sub_group_id && sub_group_id === "null" ? null : sub_group_id,
</div> !group_id && group_id === "null" ? null : group_id,
<div className="pl-0.5 pr-1 text-xs">label</div> { ...issue, priority: id }
</div> );
};
{/* assignee */} const handleLabel = (ids: string[]) => {
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center"> if (handleIssues)
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center"> handleIssues(
<Circle width={10} strokeWidth={2} /> !sub_group_id && sub_group_id === "null" ? null : sub_group_id,
</div> !group_id && group_id === "null" ? null : group_id,
<div className="pl-0.5 pr-1 text-xs">assignee</div> { ...issue, labels: ids }
</div> );
};
{/* start date */} const handleAssignee = (ids: string[]) => {
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center"> if (handleIssues)
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center"> handleIssues(
<Circle width={10} strokeWidth={2} /> !sub_group_id && sub_group_id === "null" ? null : sub_group_id,
</div> !group_id && group_id === "null" ? null : group_id,
<div className="pl-0.5 pr-1 text-xs">start date</div> { ...issue, assignees: ids }
</div> );
};
{/* target/due date */} const handleStartDate = (date: string) => {
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center"> if (handleIssues)
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center"> handleIssues(
<Circle width={10} strokeWidth={2} /> !sub_group_id && sub_group_id === "null" ? null : sub_group_id,
</div> !group_id && group_id === "null" ? null : group_id,
<div className="pl-0.5 pr-1 text-xs">target/due date</div> { ...issue, start_date: date }
</div> );
};
{/* extra render properties */} const handleTargetDate = (date: string) => {
{/* estimate */} if (handleIssues)
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center"> handleIssues(
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center"> !sub_group_id && sub_group_id === "null" ? null : sub_group_id,
<Circle width={10} strokeWidth={2} /> !group_id && group_id === "null" ? null : group_id,
</div> { ...issue, target_date: date }
<div className="pl-0.5 pr-1 text-xs">0</div> );
</div> };
{/* sub-issues */} const handleEstimate = (id: string) => {
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center"> if (handleIssues)
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center"> handleIssues(
<Circle width={10} strokeWidth={2} /> !sub_group_id && sub_group_id === "null" ? null : sub_group_id,
</div> !group_id && group_id === "null" ? null : group_id,
<div className="pl-0.5 pr-1 text-xs">0</div> { ...issue, estimate_point: id }
</div> );
};
{/* attachments */} return (
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center"> <div className="relative flex gap-2 overflow-x-auto whitespace-nowrap">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center"> {/* basic properties */}
<Circle width={10} strokeWidth={2} /> {/* state */}
</div> {display_properties && display_properties?.state && (
<div className="pl-0.5 pr-1 text-xs">0</div> <IssuePropertyState
</div> value={issue?.state || null}
dropdownArrow={false}
onChange={(id: string) => handleState(id)}
disabled={false}
/>
)}
{/* link */} {/* priority */}
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center"> {display_properties && display_properties?.priority && (
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center"> <IssuePropertyPriority
<Circle width={10} strokeWidth={2} /> value={issue?.priority || null}
</div> dropdownArrow={false}
<div className="pl-0.5 pr-1 text-xs">0</div> onChange={(id: string) => handlePriority(id)}
disabled={false}
/>
)}
{/* label */}
{display_properties && display_properties?.labels && (
<IssuePropertyLabels
value={issue?.labels || null}
dropdownArrow={false}
onChange={(ids: string[]) => handleLabel(ids)}
disabled={false}
/>
)}
{/* assignee */}
{display_properties && display_properties?.assignee && (
<IssuePropertyAssignee
value={issue?.assignees || null}
dropdownArrow={false}
onChange={(ids: string[]) => handleAssignee(ids)}
disabled={false}
/>
)}
{/* start date */}
{display_properties && display_properties?.start_date && (
<IssuePropertyStartDate
value={issue?.start_date || null}
onChange={(date: string) => handleStartDate(date)}
disabled={false}
/>
)}
{/* target/due date */}
{display_properties && display_properties?.due_date && (
<IssuePropertyStartDate
value={issue?.target_date || null}
onChange={(date: string) => handleTargetDate(date)}
disabled={false}
/>
)}
{/* estimates */}
{display_properties && display_properties?.estimate && (
<IssuePropertyEstimates
value={issue?.estimate_point?.toString() || null}
dropdownArrow={false}
onChange={(id: string) => handleEstimate(id)}
disabled={false}
workspaceSlug={issue?.workspace_detail?.slug || null}
projectId={issue?.project_detail?.id || null}
/>
)}
{/* extra render properties */}
{/* sub-issues */}
{display_properties && display_properties?.sub_issue_count && (
<Tooltip tooltipHeading="Sub-issue" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center cursor-pointer">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Layers width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">{issue.sub_issues_count}</div>
</div>
</Tooltip>
)}
{/* attachments */}
{display_properties && display_properties?.attachment_count && (
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center cursor-pointer">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Paperclip width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">{issue.attachment_count}</div>
</div>
</Tooltip>
)}
{/* link */}
{display_properties && display_properties?.link && (
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex-shrink-0 border border-custom-border-300 min-w-[22px] h-[22px] overflow-hidden rounded-sm flex justify-center items-center cursor-pointer">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Link width={10} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">{issue.link_count}</div>
</div>
</Tooltip>
)}
</div> </div>
</div> );
); }
}; );
created_on: true;
updated_on: true;
due_date: true;
key: true;

View File

@ -25,6 +25,8 @@ export const KanBanLayout: React.FC = observer(() => {
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
const display_properties = issueFilterStore?.userDisplayProperties || null;
const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by
? "swimlanes" ? "swimlanes"
: "default"; : "default";
@ -45,13 +47,29 @@ export const KanBanLayout: React.FC = observer(() => {
: issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); : issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
}; };
const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => {
issueStore.updateIssueStructure(group_by, sub_group_by, issue);
};
return ( return (
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90`}> <div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? ( {currentKanBanView === "default" ? (
<KanBan issues={issues} sub_group_by={sub_group_by} group_by={group_by} /> <KanBan
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={updateIssue}
display_properties={display_properties}
/>
) : ( ) : (
<KanBanSwimLanes issues={issues} sub_group_by={sub_group_by} group_by={group_by} /> <KanBanSwimLanes
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={updateIssue}
display_properties={display_properties}
/>
)} )}
</DragDropContext> </DragDropContext>
</div> </div>

View File

@ -54,187 +54,208 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
issues: any; issues: any;
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void;
display_properties: any;
} }
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer(({ issues, sub_group_by, group_by, list, listKey }) => { const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer(
const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore(); ({ issues, sub_group_by, group_by, list, listKey, handleIssues, display_properties }) => {
const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
const calculateIssueCount = (column_id: string) => { const calculateIssueCount = (column_id: string) => {
let issueCount = 0; let issueCount = 0;
issues?.[column_id] && issues?.[column_id] &&
Object.keys(issues?.[column_id])?.forEach((_list: any) => { Object.keys(issues?.[column_id])?.forEach((_list: any) => {
issueCount += issues?.[column_id]?.[_list]?.length || 0; issueCount += issues?.[column_id]?.[_list]?.length || 0;
}); });
return issueCount; return issueCount;
}; };
return ( return (
<div className="relative w-full min-h-full h-max"> <div className="relative w-full min-h-full h-max">
{list && {list &&
list.length > 0 && list.length > 0 &&
list.map((_list: any) => ( list.map((_list: any) => (
<div className="flex-shrink-0 flex flex-col"> <div className="flex-shrink-0 flex flex-col">
<div className="sticky top-[50px] w-full z-[1] bg-custom-background-90 flex items-center py-1"> <div className="sticky top-[50px] w-full z-[1] bg-custom-background-90 flex items-center py-1">
<div className="flex-shrink-0 sticky left-0 bg-custom-background-90 pr-2"> <div className="flex-shrink-0 sticky left-0 bg-custom-background-90 pr-2">
<KanBanSubGroupByHeaderRoot <KanBanSubGroupByHeaderRoot
column_id={getValueFromObject(_list, listKey) as string} column_id={getValueFromObject(_list, listKey) as string}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
issues_count={calculateIssueCount(getValueFromObject(_list, listKey) as string)} issues_count={calculateIssueCount(getValueFromObject(_list, listKey) as string)}
/> />
</div>
<div className="w-full border-b border-custom-border-400 border-dashed" />
</div> </div>
<div className="w-full border-b border-custom-border-400 border-dashed" /> {!issueKanBanViewStore.kanBanToggle?.subgroupByIssuesVisibility.includes(
getValueFromObject(_list, listKey) as string
) && (
<div className="relative">
<KanBan
issues={issues?.[getValueFromObject(_list, listKey) as string]}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={getValueFromObject(_list, listKey) as string}
handleIssues={handleIssues}
display_properties={display_properties}
/>
</div>
)}
</div> </div>
{!issueKanBanViewStore.kanBanToggle?.subgroupByIssuesVisibility.includes( ))}
getValueFromObject(_list, listKey) as string </div>
) && ( );
<div className="relative"> }
<KanBan );
issues={issues?.[getValueFromObject(_list, listKey) as string]}
sub_group_by={sub_group_by}
group_by={group_by}
sub_group_id={getValueFromObject(_list, listKey) as string}
/>
</div>
)}
</div>
))}
</div>
);
});
export interface IKanBanSwimLanes { export interface IKanBanSwimLanes {
issues: any; issues: any;
sub_group_by: string | null; sub_group_by: string | null;
group_by: string | null; group_by: string | null;
handleIssues?: () => void; handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void;
display_properties: any;
} }
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer(({ issues, sub_group_by, group_by }) => { export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer(
const { project: projectStore }: RootStore = useMobxStore(); ({ issues, sub_group_by, group_by, handleIssues, display_properties }) => {
const { project: projectStore }: RootStore = useMobxStore();
return ( return (
<div className="relative"> <div className="relative">
<div className="sticky top-0 z-[2] bg-custom-background-90 h-[50px]"> <div className="sticky top-0 z-[2] bg-custom-background-90 h-[50px]">
{group_by && group_by === "state" && ( {group_by && group_by === "state" && (
<SubGroupSwimlaneHeader <SubGroupSwimlaneHeader
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectStates}
listKey={`id`}
/>
)}
{group_by && group_by === "state_detail.group" && (
<SubGroupSwimlaneHeader
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={ISSUE_STATE_GROUPS}
listKey={`key`}
/>
)}
{group_by && group_by === "priority" && (
<SubGroupSwimlaneHeader
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={ISSUE_PRIORITIES}
listKey={`key`}
/>
)}
{group_by && group_by === "labels" && (
<SubGroupSwimlaneHeader
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectLabels}
listKey={`id`}
/>
)}
{group_by && group_by === "assignees" && (
<SubGroupSwimlaneHeader
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectMembers}
listKey={`member.id`}
/>
)}
{group_by && group_by === "created_by" && (
<SubGroupSwimlaneHeader
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectMembers}
listKey={`member.id`}
/>
)}
</div>
{sub_group_by && sub_group_by === "state" && (
<SubGroupSwimlane
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
list={projectStore?.projectStates} list={projectStore?.projectStates}
listKey={`id`} listKey={`id`}
handleIssues={handleIssues}
display_properties={display_properties}
/> />
)} )}
{group_by && group_by === "state_detail.group" && ( {sub_group_by && sub_group_by === "state_detail.group" && (
<SubGroupSwimlaneHeader <SubGroupSwimlane
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
list={ISSUE_STATE_GROUPS} list={ISSUE_STATE_GROUPS}
listKey={`key`} listKey={`key`}
handleIssues={handleIssues}
display_properties={display_properties}
/> />
)} )}
{group_by && group_by === "priority" && ( {sub_group_by && sub_group_by === "priority" && (
<SubGroupSwimlaneHeader <SubGroupSwimlane
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
list={ISSUE_PRIORITIES} list={ISSUE_PRIORITIES}
listKey={`key`} listKey={`key`}
handleIssues={handleIssues}
display_properties={display_properties}
/> />
)} )}
{group_by && group_by === "labels" && ( {sub_group_by && sub_group_by === "labels" && (
<SubGroupSwimlaneHeader <SubGroupSwimlane
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
list={projectStore?.projectLabels} list={projectStore?.projectLabels}
listKey={`id`} listKey={`id`}
handleIssues={handleIssues}
display_properties={display_properties}
/> />
)} )}
{group_by && group_by === "assignees" && ( {sub_group_by && sub_group_by === "assignees" && (
<SubGroupSwimlaneHeader <SubGroupSwimlane
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
list={projectStore?.projectMembers} list={projectStore?.projectMembers}
listKey={`member.id`} listKey={`member.id`}
handleIssues={handleIssues}
display_properties={display_properties}
/> />
)} )}
{group_by && group_by === "created_by" && ( {sub_group_by && sub_group_by === "created_by" && (
<SubGroupSwimlaneHeader <SubGroupSwimlane
issues={issues} issues={issues}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
list={projectStore?.projectMembers} list={projectStore?.projectMembers}
listKey={`member.id`} listKey={`member.id`}
handleIssues={handleIssues}
display_properties={display_properties}
/> />
)} )}
</div> </div>
);
{sub_group_by && sub_group_by === "state" && ( }
<SubGroupSwimlane );
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectStates}
listKey={`id`}
/>
)}
{sub_group_by && sub_group_by === "state_detail.group" && (
<SubGroupSwimlane
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={ISSUE_STATE_GROUPS}
listKey={`key`}
/>
)}
{sub_group_by && sub_group_by === "priority" && (
<SubGroupSwimlane
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={ISSUE_PRIORITIES}
listKey={`key`}
/>
)}
{sub_group_by && sub_group_by === "labels" && (
<SubGroupSwimlane
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectLabels}
listKey={`id`}
/>
)}
{sub_group_by && sub_group_by === "assignees" && (
<SubGroupSwimlane
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectMembers}
listKey={`member.id`}
/>
)}
{sub_group_by && sub_group_by === "created_by" && (
<SubGroupSwimlane
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
list={projectStore?.projectMembers}
listKey={`member.id`}
/>
)}
</div>
);
});

View File

@ -0,0 +1,266 @@
import React from "react";
// headless ui
import { Combobox } from "@headlessui/react";
// lucide icons
import { ChevronDown, Search, X, Check } from "lucide-react";
// mobx
import { observer } from "mobx-react-lite";
// components
import { Tooltip } from "components/ui";
// hooks
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
// mobx
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
interface IFiltersOption {
id: string;
title: string;
avatar: string;
}
export interface IIssuePropertyAssignee {
value?: any;
onChange?: (id: any, data: any) => void;
disabled?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
dropdownArrow?: boolean;
}
export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
({
value,
onChange,
disabled,
className,
buttonClassName,
optionsClassName,
dropdownArrow = true,
}) => {
const { project: projectStore }: RootStore = useMobxStore();
const dropdownBtn = React.useRef<any>(null);
const dropdownOptions = React.useRef<any>(null);
const [isOpen, setIsOpen] = React.useState<boolean>(false);
const [search, setSearch] = React.useState<string>("");
const options: IFiltersOption[] | [] =
(projectStore?.projectMembers &&
projectStore?.projectMembers?.length > 0 &&
projectStore?.projectMembers.map((_member: any) => ({
id: _member?.member?.id,
title: _member?.member?.display_name,
avatar: _member?.member?.avatar && _member?.member?.avatar !== "" ? _member?.member?.avatar : null,
}))) ||
[];
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
const selectedOption: IFiltersOption[] =
(value && value?.length > 0 && options.filter((_member: IFiltersOption) => value.includes(_member.id))) || [];
const filteredOptions: IFiltersOption[] =
search === ""
? options && options.length > 0
? options
: []
: options && options.length > 0
? options.filter((_member: IFiltersOption) =>
_member.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, ""))
)
: [];
const assigneeRenderLength = 5;
return (
<Combobox
multiple={true}
as="div"
className={`${className}`}
value={selectedOption.map((_member: IFiltersOption) => _member.id) as string[]}
onChange={(data: string[]) => {
if (onChange && selectedOption) onChange(data, selectedOption);
}}
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 px-1 py-0.5 rounded-sm shadow-sm border border-custom-border-300 duration-300 outline-none ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
{selectedOption && selectedOption?.length > 0 ? (
<>
{selectedOption?.length > 1 ? (
<Tooltip
tooltipHeading={`Assignees`}
tooltipContent={(selectedOption.map((_label: IFiltersOption) => _label.title) || []).join(", ")}
>
<div className="flex-shrink-0 flex justify-center items-center gap-1 pr-[8px]">
{selectedOption.slice(0, assigneeRenderLength).map((_assignee) => (
<div
key={_assignee?.id}
className="flex-shrink-0 w-[16px] h-[16px] rounded-sm bg-gray-700 flex justify-center items-center text-white capitalize relative -mr-[8px] text-xs overflow-hidden border border-custom-border-300"
>
{_assignee && _assignee.avatar ? (
<img
src={_assignee.avatar}
className="absolute top-0 left-0 h-full w-full object-cover"
alt={_assignee.title}
/>
) : (
_assignee.title[0]
)}
</div>
))}
{selectedOption.length > assigneeRenderLength && (
<div className="flex-shrink-0 h-[16px] px-0.5 rounded-sm bg-gray-700 flex justify-center items-center text-white capitalize relative -mr-[8px] text-xs overflow-hidden border border-custom-border-300">
+{selectedOption?.length - assigneeRenderLength}
</div>
)}
</div>
</Tooltip>
) : (
<Tooltip
tooltipHeading={`Assignees`}
tooltipContent={(selectedOption.map((_label: IFiltersOption) => _label.title) || []).join(", ")}
>
<div className="flex-shrink-0 flex justify-center items-center gap-1 text-xs">
<div className="flex-shrink-0 w-[14px] h-[14px] rounded-sm flex justify-center items-center text-white capitalize relative overflow-hidden text-xs">
{selectedOption[0] && selectedOption[0].avatar ? (
<img
src={selectedOption[0].avatar}
className="absolute top-0 left-0 h-full w-full object-cover"
alt={selectedOption[0].title}
/>
) : (
<div className="w-full h-full bg-gray-700 flex justify-center items-center">
{selectedOption[0].title[0]}
</div>
)}
</div>
<div className="line-clamp-1">{selectedOption[0].title}</div>
</div>
</Tooltip>
)}
</>
) : (
<div className="text-xs">Select option</div>
)}
{dropdownArrow && !disabled && (
<div className="flex-shrink-0 w-[14px] h-[14px] flex justify-center items-center">
<ChevronDown width={14} strokeWidth={2} />
</div>
)}
</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 p-2 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none whitespace-nowrap mt-1 space-y-1 ${optionsClassName}`}
>
{options && options.length > 0 ? (
<>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-1">
<div className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm">
<Search width={12} strokeWidth={2} />
</div>
<div>
<Combobox.Input
className="w-full bg-transparent p-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
{search && search.length > 0 && (
<div
onClick={() => setSearch("")}
className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80"
>
<X width={12} strokeWidth={2} />
</div>
)}
</div>
<div className={`space-y-0.5 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.id}
value={option.id}
className={({ active }) =>
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active || (value && value.length > 0 && value.includes(option?.id))
? "bg-custom-background-80"
: ""
} ${
value && value.length > 0 && value.includes(option?.id)
? "text-custom-text-100"
: "text-custom-text-200"
}`
}
>
<div className="flex items-center gap-1 w-full px-1">
<div className="flex-shrink-0 w-[18px] h-[18px] rounded-sm flex justify-center items-center text-white capitalize relative overflow-hidden">
{option && option.avatar ? (
<img
src={option.avatar}
className="absolute top-0 left-0 h-full w-full object-cover"
alt={option.title}
/>
) : (
<div className="w-full h-full bg-gray-700 flex justify-center items-center">
{option.title[0]}
</div>
)}
</div>
<div className="line-clamp-1">{option.title}</div>
{value && value.length > 0 && value.includes(option?.id) && (
<div className="flex-shrink-0 ml-auto w-[13px] h-[13px] flex justify-center items-center">
<Check width={13} strokeWidth={2} />
</div>
)}
</div>
</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>
</>
) : (
<p className="text-center text-custom-text-200">No options available.</p>
)}
</Combobox.Options>
</div>
</>
);
}}
</Combobox>
);
}
);

View File

@ -0,0 +1,96 @@
import React from "react";
// headless ui
import { Popover } from "@headlessui/react";
// lucide icons
import { Calendar, X } from "lucide-react";
// react date picker
import DatePicker from "react-datepicker";
// mobx
import { observer } from "mobx-react-lite";
// components
import { Tooltip } from "components/ui";
// hooks
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
// helpers
import { renderDateFormat } from "helpers/date-time.helper";
export interface IIssuePropertyStartDate {
value?: any;
onChange?: (date: any) => void;
disabled?: boolean;
}
export const IssuePropertyStartDate: React.FC<IIssuePropertyStartDate> = observer(({ value, onChange, disabled }) => {
const dropdownBtn = React.useRef<any>(null);
const dropdownOptions = React.useRef<any>(null);
const [isOpen, setIsOpen] = React.useState<boolean>(false);
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
return (
<Popover as="div" className="relative">
{({ open }) => {
if (open) {
if (!isOpen) setIsOpen(true);
} else if (isOpen) setIsOpen(false);
return (
<>
<Popover.Button
ref={dropdownBtn}
className={`flex items-center justify-between gap-1 px-1 py-0.5 rounded-sm shadow-sm border border-custom-border-300 duration-300 outline-none ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
}`}
>
<Tooltip tooltipHeading={`Start Date`} tooltipContent={value}>
<div className="flex-shrink-0 overflow-hidden rounded-sm flex justify-center items-center">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Calendar width={10} strokeWidth={2} />
</div>
{value ? (
<>
<div className="px-1 text-xs">{value}</div>
<div
className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center cursor-pointer"
onClick={() => {
if (onChange) onChange(null);
}}
>
<X width={10} strokeWidth={2} />
</div>
</>
) : (
<div className="text-xs">Select date</div>
)}
</div>
</Tooltip>
</Popover.Button>
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
<Popover.Panel
ref={dropdownOptions}
className={`absolute z-10 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none whitespace-nowrap mt-1`}
>
{({ close }) => (
<DatePicker
selected={value ? new Date(value) : new Date()}
onChange={(val: any) => {
if (onChange && val) {
onChange(renderDateFormat(val));
close();
}
}}
dateFormat="dd-MM-yyyy"
calendarClassName="h-full"
inline
/>
)}
</Popover.Panel>
</div>
</>
);
}}
</Popover>
);
});

View File

@ -0,0 +1,177 @@
import React from "react";
// headless ui
import { Combobox } from "@headlessui/react";
// lucide icons
import { ChevronDown, Search, X, Check } from "lucide-react";
// hooks
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
interface IFiltersOption {
id: string;
title: string;
}
export interface IIssuePropertyState {
options: IFiltersOption[];
value?: any;
onChange?: (id: any, data: IFiltersOption) => void;
disabled?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
dropdownArrow?: boolean;
children?: any;
}
export const IssuePropertyState: React.FC<IIssuePropertyState> = ({
options,
value,
onChange,
disabled,
className,
buttonClassName,
optionsClassName,
dropdownArrow = true,
children,
}) => {
const dropdownBtn = React.useRef<any>(null);
const dropdownOptions = React.useRef<any>(null);
const [isOpen, setIsOpen] = React.useState<boolean>(false);
const [search, setSearch] = React.useState<string>("");
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
const selectedOption: IFiltersOption | null | undefined =
(value && options.find((person: IFiltersOption) => person.id === value)) || null;
const filteredOptions: IFiltersOption[] =
search === ""
? options && options.length > 0
? options
: []
: options && options.length > 0
? options.filter((person: IFiltersOption) =>
person.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, ""))
)
: [];
const label = () => <div>Hello</div>;
return (
<Combobox
as="div"
className={`${className}`}
value={selectedOption && selectedOption.id}
onChange={(data: string) => {
if (onChange && selectedOption) onChange(data, selectedOption);
}}
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 px-1 py-1 rounded-sm shadow-sm border border-custom-border-300 duration-300 outline-none ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
{children ? (
children
) : (
<div className="text-xs">{(selectedOption && selectedOption?.title) || "Select option"}</div>
)}
{dropdownArrow && !disabled && (
<div className="flex-shrink-0 w-[14px] h-[14px] flex justify-center items-center">
<ChevronDown width={14} strokeWidth={2} />
</div>
)}
</Combobox.Button>
{options && options.length > 0 ? (
<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 p-2 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none min-w-48 max-w-60 whitespace-nowrap mt-1 space-y-1 ${optionsClassName}`}
>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-1">
<div className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm">
<Search width={12} strokeWidth={2} />
</div>
<div>
<Combobox.Input
className="w-full bg-transparent p-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
{search && search.length > 0 && (
<div
onClick={() => setSearch("")}
className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80"
>
<X width={12} strokeWidth={2} />
</div>
)}
</div>
<div className={`space-y-0.5 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.id}
value={option.id}
className={({ active, selected }) =>
`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 }) => (
<div className="flex justify-between items-center gap-1 w-full px-1">
<div className="line-clamp-1">{option.title}</div>
{selected && (
<div className="flex-shrink-0 w-[13px] h-[13px] flex justify-center items-center">
<Check width={13} strokeWidth={2} />
</div>
)}
</div>
)}
</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>
) : (
<p className="text-center text-custom-text-200">No options available.</p>
)}
</>
);
}}
</Combobox>
);
};

View File

@ -0,0 +1,215 @@
import React from "react";
// headless ui
import { Combobox } from "@headlessui/react";
// lucide icons
import { ChevronDown, Search, X, Check, Triangle } from "lucide-react";
// mobx
import { observer } from "mobx-react-lite";
// components
import { Tooltip } from "components/ui";
// hooks
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
// mobx
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
interface IFiltersOption {
id: string;
title: string;
key: string;
}
export interface IIssuePropertyEstimates {
value?: any;
onChange?: (id: any) => void;
disabled?: boolean;
workspaceSlug?: string;
projectId?: string;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
dropdownArrow?: boolean;
}
export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observer(
({
value,
onChange,
disabled,
workspaceSlug,
projectId,
className,
buttonClassName,
optionsClassName,
dropdownArrow = true,
}) => {
const { project: projectStore }: RootStore = useMobxStore();
const dropdownBtn = React.useRef<any>(null);
const dropdownOptions = React.useRef<any>(null);
const [isOpen, setIsOpen] = React.useState<boolean>(false);
const [search, setSearch] = React.useState<string>("");
const projectDetail =
(workspaceSlug && projectId && projectStore?.getProjectById(workspaceSlug, projectId)) || null;
const projectEstimateId = (projectDetail && projectDetail?.estimate) || null;
const estimates = (projectEstimateId && projectStore?.getProjectEstimateById(projectEstimateId)) || null;
const options: IFiltersOption[] | [] =
(estimates &&
estimates.points &&
estimates.points.length > 0 &&
estimates.points.map((_estimate) => ({
id: _estimate?.id,
title: _estimate?.value,
key: _estimate?.key.toString(),
}))) ||
[];
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
const selectedOption: IFiltersOption | null | undefined =
(value && options.find((_estimate: IFiltersOption) => _estimate.key === value)) || null;
const filteredOptions: IFiltersOption[] =
search === ""
? options && options.length > 0
? options
: []
: options && options.length > 0
? options.filter((_estimate: IFiltersOption) =>
_estimate.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, ""))
)
: [];
return (
<Combobox
as="div"
className={`${className}`}
value={selectedOption && selectedOption.key}
onChange={(data: string) => {
if (onChange) onChange(data);
}}
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 px-1 py-0.5 rounded-sm shadow-sm border border-custom-border-300 duration-300 outline-none ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
{selectedOption ? (
<Tooltip tooltipHeading={`Estimates`} tooltipContent={selectedOption?.title}>
<div className="flex-shrink-0 flex justify-center items-center gap-1">
<div className="flex-shrink-0 w-[12px] h-[12px] flex justify-center items-center">
<Triangle width={14} strokeWidth={2} />
</div>
<div className="pl-0.5 pr-1 text-xs">{selectedOption?.title}</div>
</div>
</Tooltip>
) : (
<div className="text-xs">Select option</div>
)}
{dropdownArrow && !disabled && (
<div className="flex-shrink-0 w-[14px] h-[14px] flex justify-center items-center">
<ChevronDown width={14} strokeWidth={2} />
</div>
)}
</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 p-2 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none whitespace-nowrap mt-1 space-y-1 ${optionsClassName}`}
>
{options && options.length > 0 ? (
<>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-1">
<div className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm">
<Search width={12} strokeWidth={2} />
</div>
<div>
<Combobox.Input
className="w-full bg-transparent p-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
{search && search.length > 0 && (
<div
onClick={() => setSearch("")}
className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80"
>
<X width={12} strokeWidth={2} />
</div>
)}
</div>
<div className={`space-y-0.5 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.key}
value={option.key}
className={({ active, selected }) =>
`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 }) => (
<div className="flex items-center gap-1 w-full px-1">
<div className="flex-shrink-0 w-[13px] h-[13px] flex justify-center items-center">
<Triangle width={14} strokeWidth={2} />
</div>
<div className="line-clamp-1">{option.title}</div>
{selected && (
<div className="flex-shrink-0 ml-auto w-[13px] h-[13px] flex justify-center items-center">
<Check width={13} strokeWidth={2} />
</div>
)}
</div>
)}
</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>
</>
) : (
<p className="text-center text-custom-text-200">No options available.</p>
)}
</Combobox.Options>
</div>
</>
);
}}
</Combobox>
);
}
);

View File

@ -0,0 +1,234 @@
import React from "react";
// headless ui
import { Combobox } from "@headlessui/react";
// lucide icons
import { ChevronDown, Search, X, Check } from "lucide-react";
// mobx
import { observer } from "mobx-react-lite";
// components
import { Tooltip } from "components/ui";
// hooks
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
// mobx
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
interface IFiltersOption {
id: string;
title: string;
color: string | null;
}
export interface IIssuePropertyLabels {
value?: any;
onChange?: (id: any, data: any) => void;
disabled?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
dropdownArrow?: boolean;
}
export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer(
({
value,
onChange,
disabled,
className,
buttonClassName,
optionsClassName,
dropdownArrow = true,
}) => {
const { project: projectStore }: RootStore = useMobxStore();
const dropdownBtn = React.useRef<any>(null);
const dropdownOptions = React.useRef<any>(null);
const [isOpen, setIsOpen] = React.useState<boolean>(false);
const [search, setSearch] = React.useState<string>("");
const options: IFiltersOption[] | [] =
(projectStore?.projectLabels &&
projectStore?.projectLabels?.length > 0 &&
projectStore?.projectLabels.map((_label: any) => ({
id: _label?.id,
title: _label?.name,
color: _label?.color || null,
}))) ||
[];
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
const selectedOption: IFiltersOption[] =
(value && value?.length > 0 && options.filter((_label: IFiltersOption) => value.includes(_label.id))) || [];
const filteredOptions: IFiltersOption[] =
search === ""
? options && options.length > 0
? options
: []
: options && options.length > 0
? options.filter((_label: IFiltersOption) =>
_label.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, ""))
)
: [];
return (
<Combobox
multiple={true}
as="div"
className={`${className}`}
value={selectedOption.map((_label: IFiltersOption) => _label.id) as string[]}
onChange={(data: string[]) => {
if (onChange && selectedOption) onChange(data, selectedOption);
}}
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 px-1 py-0.5 rounded-sm shadow-sm border border-custom-border-300 duration-300 outline-none ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
{selectedOption && selectedOption?.length > 0 ? (
<>
{selectedOption?.length === 1 ? (
<Tooltip
tooltipHeading={`Labels`}
tooltipContent={(selectedOption.map((_label: IFiltersOption) => _label.title) || []).join(", ")}
>
<div className="flex-shrink-0 flex justify-center items-center gap-1">
<div
className="flex-shrink-0 w-[10px] h-[10px] rounded-full"
style={{
backgroundColor: selectedOption[0]?.color || "#444",
}}
/>
<div className="pl-0.5 pr-1 text-xs">{selectedOption[0]?.title}</div>
</div>
</Tooltip>
) : (
<Tooltip
tooltipHeading={`Labels`}
tooltipContent={(selectedOption.map((_label: IFiltersOption) => _label.title) || []).join(", ")}
>
<div className="flex-shrink-0 flex justify-center items-center gap-1">
<div
className="flex-shrink-0 w-[10px] h-[10px] rounded-full"
style={{ backgroundColor: "#444" }}
/>
<div className="pl-0.5 pr-1 text-xs">{selectedOption?.length} Labels</div>
</div>
</Tooltip>
)}
</>
) : (
<div className="text-xs">Select option</div>
)}
{dropdownArrow && !disabled && (
<div className="flex-shrink-0 w-[14px] h-[14px] flex justify-center items-center">
<ChevronDown width={14} strokeWidth={2} />
</div>
)}
</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 p-2 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none whitespace-nowrap mt-1 space-y-1 ${optionsClassName}`}
>
{options && options.length > 0 ? (
<>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-1">
<div className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm">
<Search width={12} strokeWidth={2} />
</div>
<div>
<Combobox.Input
className="w-full bg-transparent p-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
{search && search.length > 0 && (
<div
onClick={() => setSearch("")}
className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80"
>
<X width={12} strokeWidth={2} />
</div>
)}
</div>
<div className={`space-y-0.5 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.id}
value={option.id}
className={({ active }) =>
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active || (value && value.length > 0 && value.includes(option?.id))
? "bg-custom-background-80"
: ""
} ${
value && value.length > 0 && value.includes(option?.id)
? "text-custom-text-100"
: "text-custom-text-200"
}`
}
>
<div className="flex items-center gap-1 w-full px-1">
<div
className="flex-shrink-0 w-[10px] h-[10px] rounded-full"
style={{
backgroundColor: option.color || "#444",
}}
/>
<div className="line-clamp-1">{option.title}</div>
{value && value.length > 0 && value.includes(option?.id) && (
<div className="flex-shrink-0 ml-auto w-[13px] h-[13px] flex justify-center items-center">
<Check width={13} strokeWidth={2} />
</div>
)}
</div>
</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>
</>
) : (
<p className="text-center text-custom-text-200">No options available.</p>
)}
</Combobox.Options>
</div>
</>
);
}}
</Combobox>
);
}
);

View File

@ -0,0 +1,224 @@
import React from "react";
// headless ui
import { Combobox } from "@headlessui/react";
// lucide icons
import { ChevronDown, Search, X, Check, AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react";
// mobx
import { observer } from "mobx-react-lite";
// components
import { Tooltip } from "components/ui";
// hooks
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
// constants
import { ISSUE_PRIORITIES } from "constants/issue";
interface IFiltersOption {
id: string;
title: string;
}
export interface IIssuePropertyPriority {
value?: any;
onChange?: (id: any, data: IFiltersOption) => void;
disabled?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
dropdownArrow?: boolean;
}
const Icon = ({ priority }: any) => (
<div className="w-full h-full">
{priority === "urgent" ? (
<div className="border border-red-500 bg-red-500 text-white w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
<AlertCircle size={12} strokeWidth={2} />
</div>
) : priority === "high" ? (
<div className="border border-red-500/20 bg-red-500/10 text-red-500 w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
<SignalHigh size={12} strokeWidth={2} className="pl-[3px]" />
</div>
) : priority === "medium" ? (
<div className="border border-orange-500/20 bg-orange-500/10 text-orange-500 w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
<SignalMedium size={12} strokeWidth={2} className="pl-[3px]" />
</div>
) : priority === "low" ? (
<div className="border border-green-500/20 bg-green-500/10 text-green-500 w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
<SignalLow size={12} strokeWidth={2} className="pl-[3px]" />
</div>
) : (
<div className="border border-custom-border-400/20 bg-custom-text-400/10 text-custom-text-400 w-full h-full overflow-hidden flex justify-center items-center rounded-sm">
<Ban size={12} strokeWidth={2} />
</div>
)}
</div>
);
export const IssuePropertyPriority: React.FC<IIssuePropertyPriority> = observer(
({
value,
onChange,
disabled,
className,
buttonClassName,
optionsClassName,
dropdownArrow = true,
}) => {
const dropdownBtn = React.useRef<any>(null);
const dropdownOptions = React.useRef<any>(null);
const [isOpen, setIsOpen] = React.useState<boolean>(false);
const [search, setSearch] = React.useState<string>("");
const options: IFiltersOption[] | [] =
(ISSUE_PRIORITIES &&
ISSUE_PRIORITIES?.length > 0 &&
ISSUE_PRIORITIES.map((_priority: any) => ({
id: _priority?.key,
title: _priority?.title,
}))) ||
[];
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
const selectedOption: IFiltersOption | null | undefined =
(value && options.find((_priority: IFiltersOption) => _priority.id === value)) || null;
const filteredOptions: IFiltersOption[] =
search === ""
? options && options.length > 0
? options
: []
: options && options.length > 0
? options.filter((_priority: IFiltersOption) =>
_priority.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, ""))
)
: [];
return (
<Combobox
as="div"
className={`${className}`}
value={selectedOption && selectedOption.id}
onChange={(data: string) => {
if (onChange && selectedOption) onChange(data, selectedOption);
}}
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 px-1 py-0.5 rounded-sm shadow-sm border border-custom-border-300 duration-300 outline-none ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
{selectedOption ? (
<Tooltip tooltipHeading={`Priority`} tooltipContent={selectedOption?.title}>
<div className="flex-shrink-0 flex justify-center items-center gap-1">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Icon priority={selectedOption?.id} />
</div>
<div className="pl-0.5 pr-1 text-xs">{selectedOption?.title}</div>
</div>
</Tooltip>
) : (
<div className="text-xs">Select option</div>
)}
{dropdownArrow && !disabled && (
<div className="flex-shrink-0 w-[14px] h-[14px] flex justify-center items-center">
<ChevronDown width={14} strokeWidth={2} />
</div>
)}
</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 p-2 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none whitespace-nowrap mt-1 space-y-1 ${optionsClassName}`}
>
{options && options.length > 0 ? (
<>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-1">
<div className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm">
<Search width={12} strokeWidth={2} />
</div>
<div>
<Combobox.Input
className="w-full bg-transparent p-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
{search && search.length > 0 && (
<div
onClick={() => setSearch("")}
className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80"
>
<X width={12} strokeWidth={2} />
</div>
)}
</div>
<div className={`space-y-0.5 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.id}
value={option.id}
className={({ active, selected }) =>
`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 }) => (
<div className="flex items-center gap-1 w-full px-1">
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
<Icon priority={option?.id} />
</div>
<div className="line-clamp-1">{option.title}</div>
{selected && (
<div className="flex-shrink-0 ml-auto w-[13px] h-[13px] flex justify-center items-center">
<Check width={13} strokeWidth={2} />
</div>
)}
</div>
)}
</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>
</>
) : (
<p className="text-center text-custom-text-200">No options available.</p>
)}
</Combobox.Options>
</div>
</>
);
}}
</Combobox>
);
}
);

View File

@ -0,0 +1,218 @@
import React from "react";
// headless ui
import { Combobox } from "@headlessui/react";
// lucide icons
import { ChevronDown, Search, X, Check } from "lucide-react";
// mobx
import { observer } from "mobx-react-lite";
// components
import { Tooltip } from "components/ui";
import { StateGroupIcon } from "components/icons";
// hooks
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
// mobx
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// types
import { IState } from "types";
interface IFiltersOption {
id: string;
title: string;
group: string;
color: string | null;
}
export interface IIssuePropertyState {
value?: any;
onChange?: (id: any, data: IFiltersOption) => void;
disabled?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
dropdownArrow?: boolean;
}
export const IssuePropertyState: React.FC<IIssuePropertyState> = observer(
({
value,
onChange,
disabled,
className,
buttonClassName,
optionsClassName,
dropdownArrow = true,
}) => {
const { project: projectStore }: RootStore = useMobxStore();
const dropdownBtn = React.useRef<any>(null);
const dropdownOptions = React.useRef<any>(null);
const [isOpen, setIsOpen] = React.useState<boolean>(false);
const [search, setSearch] = React.useState<string>("");
const options: IFiltersOption[] | [] =
(projectStore?.projectStates &&
projectStore?.projectStates?.length > 0 &&
projectStore?.projectStates.map((_state: IState) => ({
id: _state?.id,
title: _state?.name,
group: _state?.group,
color: _state?.color || null,
}))) ||
[];
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
const selectedOption: IFiltersOption | null | undefined =
(value && options.find((_state: IFiltersOption) => _state.id === value)) || null;
const filteredOptions: IFiltersOption[] =
search === ""
? options && options.length > 0
? options
: []
: options && options.length > 0
? options.filter((_state: IFiltersOption) =>
_state.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, ""))
)
: [];
return (
<Combobox
as="div"
className={`${className}`}
value={selectedOption && selectedOption.id}
onChange={(data: string) => {
if (onChange && selectedOption) onChange(data, selectedOption);
}}
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 px-1 py-0.5 rounded-sm shadow-sm border border-custom-border-300 duration-300 outline-none ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
{selectedOption ? (
<Tooltip tooltipHeading={`State`} tooltipContent={selectedOption?.title}>
<div className="flex-shrink-0 flex justify-center items-center gap-1">
<div className="flex-shrink-0 w-[12px] h-[12px] flex justify-center items-center">
<StateGroupIcon
stateGroup={selectedOption?.group as any}
color={(selectedOption?.color || null) as any}
width="12"
height="12"
/>
</div>
<div className="pl-0.5 pr-1 text-xs">{selectedOption?.title}</div>
</div>
</Tooltip>
) : (
<div className="text-xs">Select option</div>
)}
{dropdownArrow && !disabled && (
<div className="flex-shrink-0 w-[14px] h-[14px] flex justify-center items-center">
<ChevronDown width={14} strokeWidth={2} />
</div>
)}
</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 p-2 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none whitespace-nowrap mt-1 space-y-1 ${optionsClassName}`}
>
{options && options.length > 0 ? (
<>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-1">
<div className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm">
<Search width={12} strokeWidth={2} />
</div>
<div>
<Combobox.Input
className="w-full bg-transparent p-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
{search && search.length > 0 && (
<div
onClick={() => setSearch("")}
className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80"
>
<X width={12} strokeWidth={2} />
</div>
)}
</div>
<div className={`space-y-0.5 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.id}
value={option.id}
className={({ active, selected }) =>
`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 }) => (
<div className="flex items-center gap-1 w-full px-1">
<div className="flex-shrink-0 w-[13px] h-[13px] flex justify-center items-center">
<StateGroupIcon
stateGroup={option?.group as any}
color={(option?.color || null) as any}
width="13"
height="13"
/>
</div>
<div className="line-clamp-1">{option.title}</div>
{selected && (
<div className="flex-shrink-0 ml-auto w-[13px] h-[13px] flex justify-center items-center">
<Check width={13} strokeWidth={2} />
</div>
)}
</div>
)}
</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>
</>
) : (
<p className="text-center text-custom-text-200">No options available.</p>
)}
</Combobox.Options>
</div>
</>
);
}}
</Combobox>
);
}
);

View File

@ -87,83 +87,72 @@ export const StateSelect: React.FC<Props> = ({
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
return ( return (
<Combobox <div className="fixed top-16 w-72">
as="div" <Combobox value={selected} onChange={setSelected}>
className={`flex-shrink-0 text-left ${className}`} <div className="relative mt-1">
value={value.id} <div className="relative w-full cursor-default overflow-hidden rounded-lg bg-white text-left shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-teal-300 sm:text-sm">
onChange={(data: string) => { <Combobox.Input
onChange(data, states); className="w-full border-none py-2 pl-3 pr-10 text-sm leading-5 text-gray-900 focus:ring-0"
}} displayValue={(person) => person.name}
disabled={disabled} onChange={(event) => setQuery(event.target.value)}
> />
{({ open }: { open: boolean }) => { <Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
if (open) { <ChevronUpDownIcon
if (!isOpen) setIsOpen(true); className="h-5 w-5 text-gray-400"
setFetchStates(true); aria-hidden="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> </Combobox.Button>
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}> </div>
<Combobox.Options <Transition
ref={dropdownOptions} as={Fragment}
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}`} leave="transition ease-in duration-100"
> leaveFrom="opacity-100"
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2"> leaveTo="opacity-0"
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" /> afterLeave={() => setQuery('')}
<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" <Combobox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
value={query} {filteredPeople.length === 0 && query !== '' ? (
onChange={(e) => setQuery(e.target.value)} <div className="relative cursor-default select-none py-2 px-4 text-gray-700">
placeholder="Search" Nothing found.
displayValue={(assigned: any) => assigned?.name}
/>
</div> </div>
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}> ) : (
{filteredOptions ? ( filteredPeople.map((person) => (
filteredOptions.length > 0 ? ( <Combobox.Option
filteredOptions.map((option) => ( key={person.id}
<Combobox.Option className={({ active }) =>
key={option.value} `relative cursor-default select-none py-2 pl-10 pr-4 ${
value={option.value} active ? 'bg-teal-600 text-white' : 'text-gray-900'
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" : "" value={person}
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` >
} {({ selected, active }) => (
<>
<span
className={`block truncate ${
selected ? 'font-medium' : 'font-normal'
}`}
> >
{({ selected }) => ( {person.name}
<> </span>
{option.content} {selected ? (
{selected && <CheckIcon className={`h-3.5 w-3.5`} />} <span
</> className={`absolute inset-y-0 left-0 flex items-center pl-3 ${
)} active ? 'text-white' : 'text-teal-600'
</Combobox.Option> }`}
)) >
) : ( <CheckIcon className="h-5 w-5" aria-hidden="true" />
<span className="flex items-center gap-2 p-1"> </span>
<p className="text-left text-custom-text-200 ">No matching results</p> ) : null}
</span> </>
) )}
) : ( </Combobox.Option>
<p className="text-center text-custom-text-200">Loading...</p> ))
)} )}
</div> </Combobox.Options>
</Combobox.Options> </Transition>
</div> </div>
</> </Combobox>
); </div>
}}
</Combobox>
); );
}; };

View File

@ -51,17 +51,11 @@ export const Tooltip: React.FC<Props> = ({
content={ content={
<div <div
className={`relative z-50 max-w-xs gap-1 rounded-md p-2 text-xs shadow-md ${ className={`relative z-50 max-w-xs gap-1 rounded-md p-2 text-xs shadow-md ${
theme === "custom" theme === "custom" ? "bg-custom-background-100 text-custom-text-200" : "bg-black text-gray-400"
? "bg-custom-background-100 text-custom-text-200"
: "bg-black text-gray-400"
} break-words overflow-hidden ${className}`} } break-words overflow-hidden ${className}`}
> >
{tooltipHeading && ( {tooltipHeading && (
<h5 <h5 className={`font-medium ${theme === "custom" ? "text-custom-text-100" : "text-white"}`}>
className={`font-medium ${
theme === "custom" ? "text-custom-text-100" : "text-white"
}`}
>
{tooltipHeading} {tooltipHeading}
</h5> </h5>
)} )}

View File

@ -111,7 +111,7 @@ class IssueStore implements IIssueStore {
issues = issues as IIssueGroupedStructure; issues = issues as IIssueGroupedStructure;
issues = { issues = {
...issues, ...issues,
[group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? issue : i)), [group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)),
}; };
} }
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
@ -120,15 +120,17 @@ class IssueStore implements IIssueStore {
...issues, ...issues,
[sub_group_id]: { [sub_group_id]: {
...issues[sub_group_id], ...issues[sub_group_id],
[group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? issue : i)), [group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)),
}, },
}; };
} }
if (issueType === "ungrouped") { if (issueType === "ungrouped") {
issues = issues as IIssueUnGroupedStructure; issues = issues as IIssueUnGroupedStructure;
issues = issues.map((i: IIssue) => (i?.id === issue?.id ? issue : i)); issues = issues.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i));
} }
// reorder issues based on the issue update
runInAction(() => { runInAction(() => {
this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } }; this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } };
}); });

View File

@ -1,11 +1,12 @@
import { observable, action, computed, makeObservable, runInAction } from "mobx"; import { observable, action, computed, makeObservable, runInAction } from "mobx";
// types // types
import { RootStore } from "./root"; import { RootStore } from "./root";
import { IProject, IIssueLabels, IProjectMember, IStateResponse, IState } from "types"; import { IProject, IIssueLabels, IProjectMember, IStateResponse, IState, IEstimate } from "types";
// services // services
import { ProjectService } from "services/project.service"; import { ProjectService } from "services/project.service";
import { IssueService } from "services/issue.service"; import { IssueService } from "services/issue.service";
import { ProjectStateServices } from "services/project_state.service"; import { ProjectStateServices } from "services/project_state.service";
import { ProjectEstimateServices } from "services/project_estimates.service";
import { CycleService } from "services/cycles.service"; import { CycleService } from "services/cycles.service";
import { ModuleService } from "services/modules.service"; import { ModuleService } from "services/modules.service";
import { ViewService } from "services/views.service"; import { ViewService } from "services/views.service";
@ -30,6 +31,9 @@ export interface IProjectStore {
members: { members: {
[projectId: string]: IProjectMember[] | null; // project_id: members [projectId: string]: IProjectMember[] | null; // project_id: members
} | null; } | null;
estimates: {
[projectId: string]: IEstimate[] | null; // project_id: members
} | null;
// computed // computed
searchedProjects: IProject[]; searchedProjects: IProject[];
@ -37,6 +41,7 @@ export interface IProjectStore {
projectStates: IState[] | null; projectStates: IState[] | null;
projectLabels: IIssueLabels[] | null; projectLabels: IIssueLabels[] | null;
projectMembers: IProjectMember[] | null; projectMembers: IProjectMember[] | null;
projectEstimates: IEstimate[] | null;
joinedProjects: IProject[]; joinedProjects: IProject[];
favoriteProjects: IProject[]; favoriteProjects: IProject[];
@ -45,16 +50,19 @@ export interface IProjectStore {
setProjectId: (projectId: string) => void; setProjectId: (projectId: string) => void;
setSearchQuery: (query: string) => void; setSearchQuery: (query: string) => void;
getProjectById: (workspaceSlug: string, projectId: string) => IProject | null;
getProjectStateById: (stateId: string) => IState | null; getProjectStateById: (stateId: string) => IState | null;
getProjectLabelById: (labelId: string) => IIssueLabels | null; getProjectLabelById: (labelId: string) => IIssueLabels | null;
getProjectMemberById: (memberId: string) => IProjectMember | null; getProjectMemberById: (memberId: string) => IProjectMember | null;
getProjectMemberByUserId: (memberId: string) => IProjectMember | null; getProjectMemberByUserId: (memberId: string) => IProjectMember | null;
getProjectEstimateById: (estimateId: string) => IEstimate | null;
fetchProjects: (workspaceSlug: string) => Promise<void>; fetchProjects: (workspaceSlug: string) => Promise<void>;
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>; fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>;
fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise<void>; fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise<void>;
fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise<void>; fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise<void>;
fetchProjectMembers: (workspaceSlug: string, projectId: string) => Promise<void>; fetchProjectMembers: (workspaceSlug: string, projectId: string) => Promise<void>;
fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise<void>;
addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>; addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise<any>; removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
@ -88,6 +96,9 @@ class ProjectStore implements IProjectStore {
members: { members: {
[key: string]: IProjectMember[]; // project_id: members [key: string]: IProjectMember[]; // project_id: members
} | null = {}; } | null = {};
estimates: {
[key: string]: IEstimate[]; // project_id: estimates
} | null = {};
// root store // root store
rootStore; rootStore;
@ -95,6 +106,7 @@ class ProjectStore implements IProjectStore {
projectService; projectService;
issueService; issueService;
stateService; stateService;
estimateService;
constructor(_rootStore: RootStore) { constructor(_rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
@ -116,6 +128,7 @@ class ProjectStore implements IProjectStore {
projectStates: computed, projectStates: computed,
projectLabels: computed, projectLabels: computed,
projectMembers: computed, projectMembers: computed,
projectEstimates: computed,
joinedProjects: computed, joinedProjects: computed,
favoriteProjects: computed, favoriteProjects: computed,
@ -126,6 +139,7 @@ class ProjectStore implements IProjectStore {
fetchProjects: action, fetchProjects: action,
fetchProjectDetails: action, fetchProjectDetails: action,
getProjectById: action,
getProjectStateById: action, getProjectStateById: action,
getProjectLabelById: action, getProjectLabelById: action,
getProjectMemberById: action, getProjectMemberById: action,
@ -133,6 +147,7 @@ class ProjectStore implements IProjectStore {
fetchProjectStates: action, fetchProjectStates: action,
fetchProjectLabels: action, fetchProjectLabels: action,
fetchProjectMembers: action, fetchProjectMembers: action,
fetchProjectEstimates: action,
addProjectToFavorites: action, addProjectToFavorites: action,
removeProjectFromFavorites: action, removeProjectFromFavorites: action,
@ -148,6 +163,7 @@ class ProjectStore implements IProjectStore {
this.projectService = new ProjectService(); this.projectService = new ProjectService();
this.issueService = new IssueService(); this.issueService = new IssueService();
this.stateService = new ProjectStateServices(); this.stateService = new ProjectStateServices();
this.estimateService = new ProjectEstimateServices();
} }
get searchedProjects() { get searchedProjects() {
@ -202,6 +218,11 @@ class ProjectStore implements IProjectStore {
return this.members?.[this.projectId] || null; return this.members?.[this.projectId] || null;
} }
get projectEstimates() {
if (!this.projectId) return null;
return this.estimates?.[this.projectId] || null;
}
// actions // actions
setProjectId = (projectSlug: string) => { setProjectId = (projectSlug: string) => {
this.projectId = projectSlug ?? null; this.projectId = projectSlug ?? null;
@ -246,6 +267,14 @@ class ProjectStore implements IProjectStore {
} }
}; };
getProjectById = (workspaceSlug: string, projectId: string) => {
if (!this.projectId) return null;
const projects = this.projects?.[workspaceSlug];
if (!projects) return null;
const projectInfo: IProject | null = projects.find((project) => project.id === projectId) || null;
return projectInfo;
};
getProjectStateById = (stateId: string) => { getProjectStateById = (stateId: string) => {
if (!this.projectId) return null; if (!this.projectId) return null;
const states = this.projectStates; const states = this.projectStates;
@ -278,6 +307,14 @@ class ProjectStore implements IProjectStore {
return memberInfo; return memberInfo;
}; };
getProjectEstimateById = (estimateId: string) => {
if (!this.projectId) return null;
const estimates = this.projectEstimates;
if (!estimates) return null;
const estimateInfo: IEstimate | null = estimates.find((estimate) => estimate.id === estimateId) || null;
return estimateInfo;
};
fetchProjectStates = async (workspaceSlug: string, projectSlug: string) => { fetchProjectStates = async (workspaceSlug: string, projectSlug: string) => {
try { try {
this.loader = true; this.loader = true;
@ -347,6 +384,29 @@ class ProjectStore implements IProjectStore {
} }
}; };
fetchProjectEstimates = async (workspaceSlug: string, projectSlug: string) => {
try {
this.loader = true;
this.error = null;
const estimatesResponse = await this.estimateService.getEstimatesList(workspaceSlug, projectSlug);
const _estimates = {
...this.estimates,
[projectSlug]: estimatesResponse,
};
runInAction(() => {
this.estimates = _estimates;
this.loader = false;
this.error = null;
});
} catch (error) {
console.error(error);
this.loader = false;
this.error = error;
}
};
addProjectToFavorites = async (workspaceSlug: string, projectId: string) => { addProjectToFavorites = async (workspaceSlug: string, projectId: string) => {
try { try {
const response = await this.projectService.addProjectToFavorites(workspaceSlug, projectId); const response = await this.projectService.addProjectToFavorites(workspaceSlug, projectId);