mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
7be038ac5a
commit
b5b809500d
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
266
web/components/issues/issue-layouts/properties/assignee.tsx
Normal file
266
web/components/issues/issue-layouts/properties/assignee.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
96
web/components/issues/issue-layouts/properties/date.tsx
Normal file
96
web/components/issues/issue-layouts/properties/date.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
215
web/components/issues/issue-layouts/properties/estimates.tsx
Normal file
215
web/components/issues/issue-layouts/properties/estimates.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
234
web/components/issues/issue-layouts/properties/labels.tsx
Normal file
234
web/components/issues/issue-layouts/properties/labels.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
224
web/components/issues/issue-layouts/properties/priority.tsx
Normal file
224
web/components/issues/issue-layouts/properties/priority.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
218
web/components/issues/issue-layouts/properties/state.tsx
Normal file
218
web/components/issues/issue-layouts/properties/state.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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 } };
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user