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();
|
||||
|
||||
useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES` : null, async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId);
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_ISSUES` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId);
|
||||
|
||||
await projectStore.fetchProjectStates(workspaceSlug, projectId);
|
||||
await projectStore.fetchProjectLabels(workspaceSlug, projectId);
|
||||
await projectStore.fetchProjectMembers(workspaceSlug, projectId);
|
||||
await projectStore.fetchProjectStates(workspaceSlug, projectId);
|
||||
await projectStore.fetchProjectLabels(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;
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full flex flex-col overflow-auto">
|
||||
<AppliedFiltersRoot />
|
||||
{activeLayout === "kanban" ? (
|
||||
<KanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<GanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<SpreadsheetLayout />
|
||||
) : null}
|
||||
<AppliedFiltersList />
|
||||
<div className="w-full h-full">
|
||||
{activeLayout === "kanban" ? (
|
||||
<KanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<GanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<SpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -8,9 +8,18 @@ interface IssueBlockProps {
|
||||
columnId: string;
|
||||
issues: any;
|
||||
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 ? (
|
||||
<>
|
||||
@ -30,14 +39,22 @@ export const IssueBlock = ({ sub_group_id, columnId, issues, isDragDisabled }: I
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<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`
|
||||
}`}
|
||||
>
|
||||
<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="min-h-[22px]">
|
||||
<KanBanProperties />
|
||||
<div>
|
||||
<KanBanProperties
|
||||
sub_group_id={sub_group_id}
|
||||
columnId={columnId}
|
||||
issue={issue}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,12 +19,23 @@ export interface IGroupByKanBan {
|
||||
sub_group_id: string;
|
||||
list: any;
|
||||
listKey: string;
|
||||
handleIssues?: () => void;
|
||||
isDragDisabled: boolean;
|
||||
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void;
|
||||
display_properties: any;
|
||||
}
|
||||
|
||||
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 verticalAlignPosition = (_list: any) =>
|
||||
@ -42,7 +53,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
|
||||
column_id={getValueFromObject(_list, listKey) as string}
|
||||
sub_group_by={sub_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>
|
||||
)}
|
||||
@ -64,6 +75,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer(
|
||||
columnId={getValueFromObject(_list, listKey) as string}
|
||||
issues={issues[getValueFromObject(_list, listKey) as string]}
|
||||
isDragDisabled={isDragDisabled}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
) : (
|
||||
isDragDisabled && (
|
||||
@ -90,86 +103,101 @@ export interface IKanBan {
|
||||
sub_group_by: string | null;
|
||||
group_by: string | null;
|
||||
sub_group_id?: string;
|
||||
handleIssues?: () => void;
|
||||
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" }) => {
|
||||
const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
|
||||
export const KanBan: React.FC<IKanBan> = observer(
|
||||
({ issues, sub_group_by, group_by, sub_group_id = "null", handleIssues, display_properties }) => {
|
||||
const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
{group_by && group_by === "state" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={projectStore?.projectStates}
|
||||
listKey={`id`}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
/>
|
||||
)}
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
{group_by && group_by === "state" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={projectStore?.projectStates}
|
||||
listKey={`id`}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "state_detail.group" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={ISSUE_STATE_GROUPS}
|
||||
listKey={`key`}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
/>
|
||||
)}
|
||||
{group_by && group_by === "state_detail.group" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={ISSUE_STATE_GROUPS}
|
||||
listKey={`key`}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "priority" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={ISSUE_PRIORITIES}
|
||||
listKey={`key`}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
/>
|
||||
)}
|
||||
{group_by && group_by === "priority" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={ISSUE_PRIORITIES}
|
||||
listKey={`key`}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "labels" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={projectStore?.projectLabels}
|
||||
listKey={`id`}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
/>
|
||||
)}
|
||||
{group_by && group_by === "labels" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={projectStore?.projectLabels}
|
||||
listKey={`id`}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "assignees" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={projectStore?.projectMembers}
|
||||
listKey={`member.id`}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
/>
|
||||
)}
|
||||
{group_by && group_by === "assignees" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={projectStore?.projectMembers}
|
||||
listKey={`member.id`}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "created_by" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={projectStore?.projectMembers}
|
||||
listKey={`member.id`}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
{group_by && group_by === "created_by" && (
|
||||
<GroupByKanBan
|
||||
issues={issues}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
sub_group_id={sub_group_id}
|
||||
list={projectStore?.projectMembers}
|
||||
listKey={`member.id`}
|
||||
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -8,6 +8,7 @@ import { HeaderSubGroupByCard } from "./sub-group-by-card";
|
||||
// constants
|
||||
import { issuePriorityByKey } from "constants/issue";
|
||||
|
||||
|
||||
export interface IPriorityHeader {
|
||||
column_id: string;
|
||||
sub_group_by: string | null;
|
||||
|
@ -1,91 +1,205 @@
|
||||
// mobx
|
||||
import { observer } from "mobx-react-lite";
|
||||
// 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 = () => {
|
||||
console.log("properties");
|
||||
return (
|
||||
<div className="relative flex gap-2 overflow-hidden overflow-x-auto whitespace-nowrap">
|
||||
{/* basic properties */}
|
||||
{/* state */}
|
||||
<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>
|
||||
export interface IKanBanProperties {
|
||||
sub_group_id: string;
|
||||
columnId: string;
|
||||
issue: any;
|
||||
handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void;
|
||||
display_properties: any;
|
||||
}
|
||||
|
||||
{/* 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">
|
||||
<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">priority</div>
|
||||
</div>
|
||||
export const KanBanProperties: React.FC<IKanBanProperties> = observer(
|
||||
({ sub_group_id, columnId: group_id, issue, handleIssues, display_properties }) => {
|
||||
const handleState = (id: string) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, state: id }
|
||||
);
|
||||
};
|
||||
|
||||
{/* label */}
|
||||
<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">label</div>
|
||||
</div>
|
||||
const handlePriority = (id: string) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, priority: id }
|
||||
);
|
||||
};
|
||||
|
||||
{/* assignee */}
|
||||
<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">assignee</div>
|
||||
</div>
|
||||
const handleLabel = (ids: string[]) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, labels: ids }
|
||||
);
|
||||
};
|
||||
|
||||
{/* start date */}
|
||||
<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">start date</div>
|
||||
</div>
|
||||
const handleAssignee = (ids: string[]) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, assignees: ids }
|
||||
);
|
||||
};
|
||||
|
||||
{/* target/due date */}
|
||||
<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">target/due date</div>
|
||||
</div>
|
||||
const handleStartDate = (date: string) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, start_date: date }
|
||||
);
|
||||
};
|
||||
|
||||
{/* extra render properties */}
|
||||
{/* estimate */}
|
||||
<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">0</div>
|
||||
</div>
|
||||
const handleTargetDate = (date: string) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, target_date: date }
|
||||
);
|
||||
};
|
||||
|
||||
{/* sub-issues */}
|
||||
<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">0</div>
|
||||
</div>
|
||||
const handleEstimate = (id: string) => {
|
||||
if (handleIssues)
|
||||
handleIssues(
|
||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||
!group_id && group_id === "null" ? null : group_id,
|
||||
{ ...issue, estimate_point: id }
|
||||
);
|
||||
};
|
||||
|
||||
{/* attachments */}
|
||||
<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">0</div>
|
||||
</div>
|
||||
return (
|
||||
<div className="relative flex gap-2 overflow-x-auto whitespace-nowrap">
|
||||
{/* basic properties */}
|
||||
{/* state */}
|
||||
{display_properties && display_properties?.state && (
|
||||
<IssuePropertyState
|
||||
value={issue?.state || null}
|
||||
dropdownArrow={false}
|
||||
onChange={(id: string) => handleState(id)}
|
||||
disabled={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* link */}
|
||||
<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">0</div>
|
||||
{/* priority */}
|
||||
{display_properties && display_properties?.priority && (
|
||||
<IssuePropertyPriority
|
||||
value={issue?.priority || null}
|
||||
dropdownArrow={false}
|
||||
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>
|
||||
);
|
||||
};
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
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 display_properties = issueFilterStore?.userDisplayProperties || null;
|
||||
|
||||
const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by
|
||||
? "swimlanes"
|
||||
: "default";
|
||||
@ -45,13 +47,29 @@ export const KanBanLayout: React.FC = observer(() => {
|
||||
: 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 (
|
||||
<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}>
|
||||
{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>
|
||||
</div>
|
||||
|
@ -54,187 +54,208 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||
|
||||
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
||||
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 { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
|
||||
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer(
|
||||
({ issues, sub_group_by, group_by, list, listKey, handleIssues, display_properties }) => {
|
||||
const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
|
||||
|
||||
const calculateIssueCount = (column_id: string) => {
|
||||
let issueCount = 0;
|
||||
issues?.[column_id] &&
|
||||
Object.keys(issues?.[column_id])?.forEach((_list: any) => {
|
||||
issueCount += issues?.[column_id]?.[_list]?.length || 0;
|
||||
});
|
||||
return issueCount;
|
||||
};
|
||||
const calculateIssueCount = (column_id: string) => {
|
||||
let issueCount = 0;
|
||||
issues?.[column_id] &&
|
||||
Object.keys(issues?.[column_id])?.forEach((_list: any) => {
|
||||
issueCount += issues?.[column_id]?.[_list]?.length || 0;
|
||||
});
|
||||
return issueCount;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full min-h-full h-max">
|
||||
{list &&
|
||||
list.length > 0 &&
|
||||
list.map((_list: any) => (
|
||||
<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="flex-shrink-0 sticky left-0 bg-custom-background-90 pr-2">
|
||||
<KanBanSubGroupByHeaderRoot
|
||||
column_id={getValueFromObject(_list, listKey) as string}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
issues_count={calculateIssueCount(getValueFromObject(_list, listKey) as string)}
|
||||
/>
|
||||
return (
|
||||
<div className="relative w-full min-h-full h-max">
|
||||
{list &&
|
||||
list.length > 0 &&
|
||||
list.map((_list: any) => (
|
||||
<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="flex-shrink-0 sticky left-0 bg-custom-background-90 pr-2">
|
||||
<KanBanSubGroupByHeaderRoot
|
||||
column_id={getValueFromObject(_list, listKey) as string}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
issues_count={calculateIssueCount(getValueFromObject(_list, listKey) as string)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full border-b border-custom-border-400 border-dashed" />
|
||||
</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>
|
||||
{!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}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export interface IKanBanSwimLanes {
|
||||
issues: any;
|
||||
sub_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 }) => {
|
||||
const { project: projectStore }: RootStore = useMobxStore();
|
||||
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer(
|
||||
({ issues, sub_group_by, group_by, handleIssues, display_properties }) => {
|
||||
const { project: projectStore }: RootStore = useMobxStore();
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="sticky top-0 z-[2] bg-custom-background-90 h-[50px]">
|
||||
{group_by && group_by === "state" && (
|
||||
<SubGroupSwimlaneHeader
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="sticky top-0 z-[2] bg-custom-background-90 h-[50px]">
|
||||
{group_by && group_by === "state" && (
|
||||
<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}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
list={projectStore?.projectStates}
|
||||
listKey={`id`}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "state_detail.group" && (
|
||||
<SubGroupSwimlaneHeader
|
||||
{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`}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "priority" && (
|
||||
<SubGroupSwimlaneHeader
|
||||
{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`}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "labels" && (
|
||||
<SubGroupSwimlaneHeader
|
||||
{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`}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "assignees" && (
|
||||
<SubGroupSwimlaneHeader
|
||||
{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`}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group_by && group_by === "created_by" && (
|
||||
<SubGroupSwimlaneHeader
|
||||
{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`}
|
||||
handleIssues={handleIssues}
|
||||
display_properties={display_properties}
|
||||
/>
|
||||
)}
|
||||
</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);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`flex-shrink-0 text-left ${className}`}
|
||||
value={value.id}
|
||||
onChange={(data: string) => {
|
||||
onChange(data, states);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open) {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
setFetchStates(true);
|
||||
} else if (isOpen) setIsOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Button
|
||||
ref={dropdownBtn}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && <ChevronDownIcon className="h-3 w-3" aria-hidden="true" />}
|
||||
<div className="fixed top-16 w-72">
|
||||
<Combobox value={selected} onChange={setSelected}>
|
||||
<div className="relative mt-1">
|
||||
<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">
|
||||
<Combobox.Input
|
||||
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}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Combobox.Button>
|
||||
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
|
||||
<Combobox.Options
|
||||
ref={dropdownOptions}
|
||||
className={`absolute z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap mt-1 ${optionsClassName}`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
afterLeave={() => setQuery('')}
|
||||
>
|
||||
<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">
|
||||
{filteredPeople.length === 0 && query !== '' ? (
|
||||
<div className="relative cursor-default select-none py-2 px-4 text-gray-700">
|
||||
Nothing found.
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active && !selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
) : (
|
||||
filteredPeople.map((person) => (
|
||||
<Combobox.Option
|
||||
key={person.id}
|
||||
className={({ active }) =>
|
||||
`relative cursor-default select-none py-2 pl-10 pr-4 ${
|
||||
active ? 'bg-teal-600 text-white' : 'text-gray-900'
|
||||
}`
|
||||
}
|
||||
value={person}
|
||||
>
|
||||
{({ selected, active }) => (
|
||||
<>
|
||||
<span
|
||||
className={`block truncate ${
|
||||
selected ? 'font-medium' : 'font-normal'
|
||||
}`}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <CheckIcon className={`h-3.5 w-3.5`} />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Combobox>
|
||||
{person.name}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span
|
||||
className={`absolute inset-y-0 left-0 flex items-center pl-3 ${
|
||||
active ? 'text-white' : 'text-teal-600'
|
||||
}`}
|
||||
>
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
)}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Combobox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -51,17 +51,11 @@ export const Tooltip: React.FC<Props> = ({
|
||||
content={
|
||||
<div
|
||||
className={`relative z-50 max-w-xs gap-1 rounded-md p-2 text-xs shadow-md ${
|
||||
theme === "custom"
|
||||
? "bg-custom-background-100 text-custom-text-200"
|
||||
: "bg-black text-gray-400"
|
||||
theme === "custom" ? "bg-custom-background-100 text-custom-text-200" : "bg-black text-gray-400"
|
||||
} break-words overflow-hidden ${className}`}
|
||||
>
|
||||
{tooltipHeading && (
|
||||
<h5
|
||||
className={`font-medium ${
|
||||
theme === "custom" ? "text-custom-text-100" : "text-white"
|
||||
}`}
|
||||
>
|
||||
<h5 className={`font-medium ${theme === "custom" ? "text-custom-text-100" : "text-white"}`}>
|
||||
{tooltipHeading}
|
||||
</h5>
|
||||
)}
|
||||
|
@ -111,7 +111,7 @@ class IssueStore implements IIssueStore {
|
||||
issues = issues as IIssueGroupedStructure;
|
||||
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) {
|
||||
@ -120,15 +120,17 @@ class IssueStore implements IIssueStore {
|
||||
...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") {
|
||||
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(() => {
|
||||
this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } };
|
||||
});
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||
// types
|
||||
import { RootStore } from "./root";
|
||||
import { IProject, IIssueLabels, IProjectMember, IStateResponse, IState } from "types";
|
||||
import { IProject, IIssueLabels, IProjectMember, IStateResponse, IState, IEstimate } from "types";
|
||||
// services
|
||||
import { ProjectService } from "services/project.service";
|
||||
import { IssueService } from "services/issue.service";
|
||||
import { ProjectStateServices } from "services/project_state.service";
|
||||
import { ProjectEstimateServices } from "services/project_estimates.service";
|
||||
import { CycleService } from "services/cycles.service";
|
||||
import { ModuleService } from "services/modules.service";
|
||||
import { ViewService } from "services/views.service";
|
||||
@ -30,6 +31,9 @@ export interface IProjectStore {
|
||||
members: {
|
||||
[projectId: string]: IProjectMember[] | null; // project_id: members
|
||||
} | null;
|
||||
estimates: {
|
||||
[projectId: string]: IEstimate[] | null; // project_id: members
|
||||
} | null;
|
||||
|
||||
// computed
|
||||
searchedProjects: IProject[];
|
||||
@ -37,6 +41,7 @@ export interface IProjectStore {
|
||||
projectStates: IState[] | null;
|
||||
projectLabels: IIssueLabels[] | null;
|
||||
projectMembers: IProjectMember[] | null;
|
||||
projectEstimates: IEstimate[] | null;
|
||||
|
||||
joinedProjects: IProject[];
|
||||
favoriteProjects: IProject[];
|
||||
@ -45,16 +50,19 @@ export interface IProjectStore {
|
||||
setProjectId: (projectId: string) => void;
|
||||
setSearchQuery: (query: string) => void;
|
||||
|
||||
getProjectById: (workspaceSlug: string, projectId: string) => IProject | null;
|
||||
getProjectStateById: (stateId: string) => IState | null;
|
||||
getProjectLabelById: (labelId: string) => IIssueLabels | null;
|
||||
getProjectMemberById: (memberId: string) => IProjectMember | null;
|
||||
getProjectMemberByUserId: (memberId: string) => IProjectMember | null;
|
||||
getProjectEstimateById: (estimateId: string) => IEstimate | null;
|
||||
|
||||
fetchProjects: (workspaceSlug: string) => Promise<void>;
|
||||
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||
fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
fetchProjectLabels: (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>;
|
||||
removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||
@ -88,6 +96,9 @@ class ProjectStore implements IProjectStore {
|
||||
members: {
|
||||
[key: string]: IProjectMember[]; // project_id: members
|
||||
} | null = {};
|
||||
estimates: {
|
||||
[key: string]: IEstimate[]; // project_id: estimates
|
||||
} | null = {};
|
||||
|
||||
// root store
|
||||
rootStore;
|
||||
@ -95,6 +106,7 @@ class ProjectStore implements IProjectStore {
|
||||
projectService;
|
||||
issueService;
|
||||
stateService;
|
||||
estimateService;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
@ -116,6 +128,7 @@ class ProjectStore implements IProjectStore {
|
||||
projectStates: computed,
|
||||
projectLabels: computed,
|
||||
projectMembers: computed,
|
||||
projectEstimates: computed,
|
||||
|
||||
joinedProjects: computed,
|
||||
favoriteProjects: computed,
|
||||
@ -126,6 +139,7 @@ class ProjectStore implements IProjectStore {
|
||||
fetchProjects: action,
|
||||
fetchProjectDetails: action,
|
||||
|
||||
getProjectById: action,
|
||||
getProjectStateById: action,
|
||||
getProjectLabelById: action,
|
||||
getProjectMemberById: action,
|
||||
@ -133,6 +147,7 @@ class ProjectStore implements IProjectStore {
|
||||
fetchProjectStates: action,
|
||||
fetchProjectLabels: action,
|
||||
fetchProjectMembers: action,
|
||||
fetchProjectEstimates: action,
|
||||
|
||||
addProjectToFavorites: action,
|
||||
removeProjectFromFavorites: action,
|
||||
@ -148,6 +163,7 @@ class ProjectStore implements IProjectStore {
|
||||
this.projectService = new ProjectService();
|
||||
this.issueService = new IssueService();
|
||||
this.stateService = new ProjectStateServices();
|
||||
this.estimateService = new ProjectEstimateServices();
|
||||
}
|
||||
|
||||
get searchedProjects() {
|
||||
@ -202,6 +218,11 @@ class ProjectStore implements IProjectStore {
|
||||
return this.members?.[this.projectId] || null;
|
||||
}
|
||||
|
||||
get projectEstimates() {
|
||||
if (!this.projectId) return null;
|
||||
return this.estimates?.[this.projectId] || null;
|
||||
}
|
||||
|
||||
// actions
|
||||
setProjectId = (projectSlug: string) => {
|
||||
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) => {
|
||||
if (!this.projectId) return null;
|
||||
const states = this.projectStates;
|
||||
@ -278,6 +307,14 @@ class ProjectStore implements IProjectStore {
|
||||
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) => {
|
||||
try {
|
||||
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) => {
|
||||
try {
|
||||
const response = await this.projectService.addProjectToFavorites(workspaceSlug, projectId);
|
||||
|
Loading…
Reference in New Issue
Block a user