diff --git a/web/components/core/views/all-views.tsx b/web/components/core/views/all-views.tsx
index 85830ee27..a8d6f929a 100644
--- a/web/components/core/views/all-views.tsx
+++ b/web/components/core/views/all-views.tsx
@@ -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 (
-
- {activeLayout === "kanban" ? (
-
- ) : activeLayout === "calendar" ? (
-
- ) : activeLayout === "gantt_chart" ? (
-
- ) : activeLayout === "spreadsheet" ? (
-
- ) : null}
+
+
+ {activeLayout === "kanban" ? (
+
+ ) : activeLayout === "calendar" ? (
+
+ ) : activeLayout === "gantt_chart" ? (
+
+ ) : activeLayout === "spreadsheet" ? (
+
+ ) : null}
+
);
});
diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx
index ad424df25..c65680dc1 100644
--- a/web/components/issues/issue-layouts/kanban/block.tsx
+++ b/web/components/issues/issue-layouts/kanban/block.tsx
@@ -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}
>
-
ONE-{issue.sequence_id}
+ {display_properties && display_properties?.key && (
+
ONE-{issue.sequence_id}
+ )}
{issue.name}
-
diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx
index 842550e40..395e93ea8 100644
--- a/web/components/issues/issue-layouts/kanban/default.tsx
+++ b/web/components/issues/issue-layouts/kanban/default.tsx
@@ -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 = 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 = 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}
/>
)}
@@ -64,6 +75,8 @@ const GroupByKanBan: React.FC = 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 = observer(({ issues, sub_group_by, group_by, sub_group_id = "null" }) => {
- const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
+export const KanBan: React.FC = observer(
+ ({ issues, sub_group_by, group_by, sub_group_id = "null", handleIssues, display_properties }) => {
+ const { project: projectStore, issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
- return (
-
- {group_by && group_by === "state" && (
-
- )}
+ return (
+
+ {group_by && group_by === "state" && (
+
+ )}
- {group_by && group_by === "state_detail.group" && (
-
- )}
+ {group_by && group_by === "state_detail.group" && (
+
+ )}
- {group_by && group_by === "priority" && (
-
- )}
+ {group_by && group_by === "priority" && (
+
+ )}
- {group_by && group_by === "labels" && (
-
- )}
+ {group_by && group_by === "labels" && (
+
+ )}
- {group_by && group_by === "assignees" && (
-
- )}
+ {group_by && group_by === "assignees" && (
+
+ )}
- {group_by && group_by === "created_by" && (
-
- )}
-
- );
-});
+ {group_by && group_by === "created_by" && (
+
+ )}
+
+ );
+ }
+);
diff --git a/web/components/issues/issue-layouts/kanban/headers/priority.tsx b/web/components/issues/issue-layouts/kanban/headers/priority.tsx
index 38e4afbc4..91820530b 100644
--- a/web/components/issues/issue-layouts/kanban/headers/priority.tsx
+++ b/web/components/issues/issue-layouts/kanban/headers/priority.tsx
@@ -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;
diff --git a/web/components/issues/issue-layouts/kanban/properties.tsx b/web/components/issues/issue-layouts/kanban/properties.tsx
index b2a8ce8ff..03df73fca 100644
--- a/web/components/issues/issue-layouts/kanban/properties.tsx
+++ b/web/components/issues/issue-layouts/kanban/properties.tsx
@@ -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 (
-
- {/* basic properties */}
- {/* state */}
-
+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 */}
-
+export const KanBanProperties: React.FC
= 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 */}
-
+ 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 */}
-
+ 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 */}
-
+ 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 */}
-
-
-
-
-
target/due date
-
+ 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 */}
-
+ 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 */}
-
+ 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 */}
-
+ return (
+
+ {/* basic properties */}
+ {/* state */}
+ {display_properties && display_properties?.state && (
+
handleState(id)}
+ disabled={false}
+ />
+ )}
- {/* link */}
-
-
-
-
-
0
+ {/* priority */}
+ {display_properties && display_properties?.priority && (
+
handlePriority(id)}
+ disabled={false}
+ />
+ )}
+
+ {/* label */}
+ {display_properties && display_properties?.labels && (
+ handleLabel(ids)}
+ disabled={false}
+ />
+ )}
+
+ {/* assignee */}
+ {display_properties && display_properties?.assignee && (
+ handleAssignee(ids)}
+ disabled={false}
+ />
+ )}
+
+ {/* start date */}
+ {display_properties && display_properties?.start_date && (
+ handleStartDate(date)}
+ disabled={false}
+ />
+ )}
+
+ {/* target/due date */}
+ {display_properties && display_properties?.due_date && (
+ handleTargetDate(date)}
+ disabled={false}
+ />
+ )}
+
+ {/* estimates */}
+ {display_properties && display_properties?.estimate && (
+ 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 && (
+
+
+
+
+
+
{issue.sub_issues_count}
+
+
+ )}
+
+ {/* attachments */}
+ {display_properties && display_properties?.attachment_count && (
+
+
+
+
{issue.attachment_count}
+
+
+ )}
+
+ {/* link */}
+ {display_properties && display_properties?.link && (
+
+
+
+
+
+
{issue.link_count}
+
+
+ )}
-
- );
-};
+ );
+ }
+);
+
+created_on: true;
+updated_on: true;
+due_date: true;
+
+key: true;
diff --git a/web/components/issues/issue-layouts/kanban/root.tsx b/web/components/issues/issue-layouts/kanban/root.tsx
index 5701a297d..4d72936c0 100644
--- a/web/components/issues/issue-layouts/kanban/root.tsx
+++ b/web/components/issues/issue-layouts/kanban/root.tsx
@@ -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 (
-
+
{currentKanBanView === "default" ? (
-
+
) : (
-
+
)}
diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx
index 5579b33cd..6f0dee695 100644
--- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx
+++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx
@@ -54,187 +54,208 @@ const SubGroupSwimlaneHeader: React.FC
= ({
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 = observer(({ issues, sub_group_by, group_by, list, listKey }) => {
- const { issueKanBanView: issueKanBanViewStore }: RootStore = useMobxStore();
+const SubGroupSwimlane: React.FC = 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 (
-
- {list &&
- list.length > 0 &&
- list.map((_list: any) => (
-
-
-
-
+ return (
+
+ {list &&
+ list.length > 0 &&
+ list.map((_list: any) => (
+
+
-
+ {!issueKanBanViewStore.kanBanToggle?.subgroupByIssuesVisibility.includes(
+ getValueFromObject(_list, listKey) as string
+ ) && (
+
+
+
+ )}
- {!issueKanBanViewStore.kanBanToggle?.subgroupByIssuesVisibility.includes(
- getValueFromObject(_list, listKey) as string
- ) && (
-
-
-
- )}
-
- ))}
-
- );
-});
+ ))}
+
+ );
+ }
+);
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
= observer(({ issues, sub_group_by, group_by }) => {
- const { project: projectStore }: RootStore = useMobxStore();
+export const KanBanSwimLanes: React.FC = observer(
+ ({ issues, sub_group_by, group_by, handleIssues, display_properties }) => {
+ const { project: projectStore }: RootStore = useMobxStore();
- return (
-
-
- {group_by && group_by === "state" && (
-
+
+ {group_by && group_by === "state" && (
+
+ )}
+
+ {group_by && group_by === "state_detail.group" && (
+
+ )}
+
+ {group_by && group_by === "priority" && (
+
+ )}
+
+ {group_by && group_by === "labels" && (
+
+ )}
+
+ {group_by && group_by === "assignees" && (
+
+ )}
+
+ {group_by && group_by === "created_by" && (
+
+ )}
+
+
+ {sub_group_by && sub_group_by === "state" && (
+
)}
- {group_by && group_by === "state_detail.group" && (
-
)}
- {group_by && group_by === "priority" && (
-
)}
- {group_by && group_by === "labels" && (
-
)}
- {group_by && group_by === "assignees" && (
-
)}
- {group_by && group_by === "created_by" && (
-
)}
-
- {sub_group_by && sub_group_by === "state" && (
-
- )}
-
- {sub_group_by && sub_group_by === "state_detail.group" && (
-
- )}
-
- {sub_group_by && sub_group_by === "priority" && (
-
- )}
-
- {sub_group_by && sub_group_by === "labels" && (
-
- )}
-
- {sub_group_by && sub_group_by === "assignees" && (
-
- )}
-
- {sub_group_by && sub_group_by === "created_by" && (
-
- )}
-
- );
-});
+ );
+ }
+);
diff --git a/web/components/issues/issue-layouts/properties/assignee.tsx b/web/components/issues/issue-layouts/properties/assignee.tsx
new file mode 100644
index 000000000..26ec6f204
--- /dev/null
+++ b/web/components/issues/issue-layouts/properties/assignee.tsx
@@ -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 = observer(
+ ({
+ value,
+ onChange,
+ disabled,
+
+ className,
+ buttonClassName,
+ optionsClassName,
+ dropdownArrow = true,
+ }) => {
+ const { project: projectStore }: RootStore = useMobxStore();
+
+ const dropdownBtn = React.useRef(null);
+ const dropdownOptions = React.useRef(null);
+
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [search, setSearch] = React.useState("");
+
+ 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 (
+ _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 (
+ <>
+
+ {selectedOption && selectedOption?.length > 0 ? (
+ <>
+ {selectedOption?.length > 1 ? (
+ _label.title) || []).join(", ")}
+ >
+
+ {selectedOption.slice(0, assigneeRenderLength).map((_assignee) => (
+
+ {_assignee && _assignee.avatar ? (
+
+ ) : (
+ _assignee.title[0]
+ )}
+
+ ))}
+ {selectedOption.length > assigneeRenderLength && (
+
+ +{selectedOption?.length - assigneeRenderLength}
+
+ )}
+
+
+ ) : (
+ _label.title) || []).join(", ")}
+ >
+
+
+ {selectedOption[0] && selectedOption[0].avatar ? (
+
+ ) : (
+
+ {selectedOption[0].title[0]}
+
+ )}
+
+
{selectedOption[0].title}
+
+
+ )}
+ >
+ ) : (
+ Select option
+ )}
+
+ {dropdownArrow && !disabled && (
+
+
+
+ )}
+
+
+
+
+ {options && options.length > 0 ? (
+ <>
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search"
+ displayValue={(assigned: any) => assigned?.name}
+ />
+
+
+ {search && search.length > 0 && (
+
setSearch("")}
+ className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80"
+ >
+
+
+ )}
+
+
+
+ {filteredOptions ? (
+ filteredOptions.length > 0 ? (
+ filteredOptions.map((option) => (
+
+ `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"
+ }`
+ }
+ >
+
+
+ {option && option.avatar ? (
+
+ ) : (
+
+ {option.title[0]}
+
+ )}
+
+
{option.title}
+ {value && value.length > 0 && value.includes(option?.id) && (
+
+
+
+ )}
+
+
+ ))
+ ) : (
+
+ No matching results
+
+ )
+ ) : (
+
Loading...
+ )}
+
+ >
+ ) : (
+ No options available.
+ )}
+
+
+ >
+ );
+ }}
+
+ );
+ }
+);
diff --git a/web/components/issues/issue-layouts/properties/date.tsx b/web/components/issues/issue-layouts/properties/date.tsx
new file mode 100644
index 000000000..37853ced1
--- /dev/null
+++ b/web/components/issues/issue-layouts/properties/date.tsx
@@ -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 = observer(({ value, onChange, disabled }) => {
+ const dropdownBtn = React.useRef(null);
+ const dropdownOptions = React.useRef(null);
+
+ const [isOpen, setIsOpen] = React.useState(false);
+
+ useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
+
+ return (
+
+ {({ open }) => {
+ if (open) {
+ if (!isOpen) setIsOpen(true);
+ } else if (isOpen) setIsOpen(false);
+
+ return (
+ <>
+
+
+
+
+
+
+ {value ? (
+ <>
+
{value}
+
{
+ if (onChange) onChange(null);
+ }}
+ >
+
+
+ >
+ ) : (
+
Select date
+ )}
+
+
+
+
+
+
+ {({ close }) => (
+ {
+ if (onChange && val) {
+ onChange(renderDateFormat(val));
+ close();
+ }
+ }}
+ dateFormat="dd-MM-yyyy"
+ calendarClassName="h-full"
+ inline
+ />
+ )}
+
+
+ >
+ );
+ }}
+
+ );
+});
diff --git a/web/components/issues/issue-layouts/properties/dropdown-template.tsx b/web/components/issues/issue-layouts/properties/dropdown-template.tsx
new file mode 100644
index 000000000..4afc04cb0
--- /dev/null
+++ b/web/components/issues/issue-layouts/properties/dropdown-template.tsx
@@ -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 = ({
+ options,
+ value,
+ onChange,
+ disabled,
+
+ className,
+ buttonClassName,
+ optionsClassName,
+ dropdownArrow = true,
+
+ children,
+}) => {
+ const dropdownBtn = React.useRef(null);
+ const dropdownOptions = React.useRef(null);
+
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [search, setSearch] = React.useState("");
+
+ 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 = () => Hello
;
+
+ return (
+ {
+ if (onChange && selectedOption) onChange(data, selectedOption);
+ }}
+ disabled={disabled}
+ >
+ {({ open }: { open: boolean }) => {
+ if (open) {
+ if (!isOpen) setIsOpen(true);
+ } else if (isOpen) setIsOpen(false);
+
+ return (
+ <>
+
+ {children ? (
+ children
+ ) : (
+ {(selectedOption && selectedOption?.title) || "Select option"}
+ )}
+
+ {dropdownArrow && !disabled && (
+
+
+
+ )}
+
+
+ {options && options.length > 0 ? (
+
+
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search"
+ displayValue={(assigned: any) => assigned?.name}
+ />
+
+
+ {search && search.length > 0 && (
+
setSearch("")}
+ className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80"
+ >
+
+
+ )}
+
+
+
+ {filteredOptions ? (
+ filteredOptions.length > 0 ? (
+ filteredOptions.map((option) => (
+
+ `cursor-pointer select-none truncate rounded px-1 py-1.5 ${
+ active || selected ? "bg-custom-background-80" : ""
+ } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
+ }
+ >
+ {({ selected }) => (
+
+
{option.title}
+ {selected && (
+
+
+
+ )}
+
+ )}
+
+ ))
+ ) : (
+
+ No matching results
+
+ )
+ ) : (
+
Loading...
+ )}
+
+
+
+ ) : (
+ No options available.
+ )}
+ >
+ );
+ }}
+
+ );
+};
diff --git a/web/components/issues/issue-layouts/properties/estimates.tsx b/web/components/issues/issue-layouts/properties/estimates.tsx
new file mode 100644
index 000000000..1647d1a16
--- /dev/null
+++ b/web/components/issues/issue-layouts/properties/estimates.tsx
@@ -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 = observer(
+ ({
+ value,
+ onChange,
+ disabled,
+
+ workspaceSlug,
+ projectId,
+
+ className,
+ buttonClassName,
+ optionsClassName,
+ dropdownArrow = true,
+ }) => {
+ const { project: projectStore }: RootStore = useMobxStore();
+
+ const dropdownBtn = React.useRef(null);
+ const dropdownOptions = React.useRef(null);
+
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [search, setSearch] = React.useState("");
+
+ 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 (
+ {
+ if (onChange) onChange(data);
+ }}
+ disabled={disabled}
+ >
+ {({ open }: { open: boolean }) => {
+ if (open) {
+ if (!isOpen) setIsOpen(true);
+ } else if (isOpen) setIsOpen(false);
+
+ return (
+ <>
+
+ {selectedOption ? (
+
+
+
+
+
+
{selectedOption?.title}
+
+
+ ) : (
+ Select option
+ )}
+
+ {dropdownArrow && !disabled && (
+
+
+
+ )}
+
+
+
+
+ {options && options.length > 0 ? (
+ <>
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search"
+ displayValue={(assigned: any) => assigned?.name}
+ />
+
+
+ {search && search.length > 0 && (
+
setSearch("")}
+ className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80"
+ >
+
+
+ )}
+
+
+
+ {filteredOptions ? (
+ filteredOptions.length > 0 ? (
+ filteredOptions.map((option) => (
+
+ `cursor-pointer select-none truncate rounded px-1 py-1.5 ${
+ active || selected ? "bg-custom-background-80" : ""
+ } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
+ }
+ >
+ {({ selected }) => (
+
+
+
+
+
{option.title}
+ {selected && (
+
+
+
+ )}
+
+ )}
+
+ ))
+ ) : (
+
+ No matching results
+
+ )
+ ) : (
+
Loading...
+ )}
+
+ >
+ ) : (
+ No options available.
+ )}
+
+
+ >
+ );
+ }}
+
+ );
+ }
+);
diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx
new file mode 100644
index 000000000..c1f60f909
--- /dev/null
+++ b/web/components/issues/issue-layouts/properties/labels.tsx
@@ -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 = observer(
+ ({
+ value,
+ onChange,
+ disabled,
+
+ className,
+ buttonClassName,
+ optionsClassName,
+ dropdownArrow = true,
+ }) => {
+ const { project: projectStore }: RootStore = useMobxStore();
+
+ const dropdownBtn = React.useRef(null);
+ const dropdownOptions = React.useRef(null);
+
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [search, setSearch] = React.useState("");
+
+ 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 (
+ _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 (
+ <>
+
+ {selectedOption && selectedOption?.length > 0 ? (
+ <>
+ {selectedOption?.length === 1 ? (
+ _label.title) || []).join(", ")}
+ >
+
+
+
{selectedOption[0]?.title}
+
+
+ ) : (
+ _label.title) || []).join(", ")}
+ >
+
+
+
{selectedOption?.length} Labels
+
+
+ )}
+ >
+ ) : (
+ Select option
+ )}
+
+ {dropdownArrow && !disabled && (
+
+
+
+ )}
+
+
+
+
+ {options && options.length > 0 ? (
+ <>
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search"
+ displayValue={(assigned: any) => assigned?.name}
+ />
+
+
+ {search && search.length > 0 && (
+
setSearch("")}
+ className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80"
+ >
+
+
+ )}
+
+
+
+ {filteredOptions ? (
+ filteredOptions.length > 0 ? (
+ filteredOptions.map((option) => (
+
+ `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"
+ }`
+ }
+ >
+
+
+
{option.title}
+ {value && value.length > 0 && value.includes(option?.id) && (
+
+
+
+ )}
+
+
+ ))
+ ) : (
+
+ No matching results
+
+ )
+ ) : (
+
Loading...
+ )}
+
+ >
+ ) : (
+ No options available.
+ )}
+
+
+ >
+ );
+ }}
+
+ );
+ }
+);
diff --git a/web/components/issues/issue-layouts/properties/priority.tsx b/web/components/issues/issue-layouts/properties/priority.tsx
new file mode 100644
index 000000000..a6776633a
--- /dev/null
+++ b/web/components/issues/issue-layouts/properties/priority.tsx
@@ -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) => (
+
+ {priority === "urgent" ? (
+
+ ) : priority === "high" ? (
+
+
+
+ ) : priority === "medium" ? (
+
+
+
+ ) : priority === "low" ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+);
+
+export const IssuePropertyPriority: React.FC = observer(
+ ({
+ value,
+ onChange,
+ disabled,
+
+ className,
+ buttonClassName,
+ optionsClassName,
+ dropdownArrow = true,
+ }) => {
+ const dropdownBtn = React.useRef(null);
+ const dropdownOptions = React.useRef(null);
+
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [search, setSearch] = React.useState("");
+
+ 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 (
+ {
+ if (onChange && selectedOption) onChange(data, selectedOption);
+ }}
+ disabled={disabled}
+ >
+ {({ open }: { open: boolean }) => {
+ if (open) {
+ if (!isOpen) setIsOpen(true);
+ } else if (isOpen) setIsOpen(false);
+
+ return (
+ <>
+
+ {selectedOption ? (
+
+
+
+
+
+
{selectedOption?.title}
+
+
+ ) : (
+ Select option
+ )}
+
+ {dropdownArrow && !disabled && (
+
+
+
+ )}
+
+
+
+
+ {options && options.length > 0 ? (
+ <>
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search"
+ displayValue={(assigned: any) => assigned?.name}
+ />
+
+
+ {search && search.length > 0 && (
+
setSearch("")}
+ className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80"
+ >
+
+
+ )}
+
+
+
+ {filteredOptions ? (
+ filteredOptions.length > 0 ? (
+ filteredOptions.map((option) => (
+
+ `cursor-pointer select-none truncate rounded px-1 py-1.5 ${
+ active || selected ? "bg-custom-background-80" : ""
+ } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
+ }
+ >
+ {({ selected }) => (
+
+
+
+
+
{option.title}
+ {selected && (
+
+
+
+ )}
+
+ )}
+
+ ))
+ ) : (
+
+ No matching results
+
+ )
+ ) : (
+
Loading...
+ )}
+
+ >
+ ) : (
+ No options available.
+ )}
+
+
+ >
+ );
+ }}
+
+ );
+ }
+);
diff --git a/web/components/issues/issue-layouts/properties/state.tsx b/web/components/issues/issue-layouts/properties/state.tsx
new file mode 100644
index 000000000..28ed1fe0e
--- /dev/null
+++ b/web/components/issues/issue-layouts/properties/state.tsx
@@ -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 = observer(
+ ({
+ value,
+ onChange,
+ disabled,
+
+ className,
+ buttonClassName,
+ optionsClassName,
+ dropdownArrow = true,
+ }) => {
+ const { project: projectStore }: RootStore = useMobxStore();
+
+ const dropdownBtn = React.useRef(null);
+ const dropdownOptions = React.useRef(null);
+
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [search, setSearch] = React.useState("");
+
+ 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 (
+ {
+ if (onChange && selectedOption) onChange(data, selectedOption);
+ }}
+ disabled={disabled}
+ >
+ {({ open }: { open: boolean }) => {
+ if (open) {
+ if (!isOpen) setIsOpen(true);
+ } else if (isOpen) setIsOpen(false);
+
+ return (
+ <>
+
+ {selectedOption ? (
+
+
+
+
+
+
{selectedOption?.title}
+
+
+ ) : (
+ Select option
+ )}
+
+ {dropdownArrow && !disabled && (
+
+
+
+ )}
+
+
+
+
+ {options && options.length > 0 ? (
+ <>
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search"
+ displayValue={(assigned: any) => assigned?.name}
+ />
+
+
+ {search && search.length > 0 && (
+
setSearch("")}
+ className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80"
+ >
+
+
+ )}
+
+
+
+ {filteredOptions ? (
+ filteredOptions.length > 0 ? (
+ filteredOptions.map((option) => (
+
+ `cursor-pointer select-none truncate rounded px-1 py-1.5 ${
+ active || selected ? "bg-custom-background-80" : ""
+ } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
+ }
+ >
+ {({ selected }) => (
+
+
+
+
+
{option.title}
+ {selected && (
+
+
+
+ )}
+
+ )}
+
+ ))
+ ) : (
+
+ No matching results
+
+ )
+ ) : (
+
Loading...
+ )}
+
+ >
+ ) : (
+ No options available.
+ )}
+
+
+ >
+ );
+ }}
+
+ );
+ }
+);
diff --git a/web/components/states/state-select.tsx b/web/components/states/state-select.tsx
index 22e4ef115..02c2ea365 100644
--- a/web/components/states/state-select.tsx
+++ b/web/components/states/state-select.tsx
@@ -87,83 +87,72 @@ export const StateSelect: React.FC = ({
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
return (
- {
- onChange(data, states);
- }}
- disabled={disabled}
- >
- {({ open }: { open: boolean }) => {
- if (open) {
- if (!isOpen) setIsOpen(true);
- setFetchStates(true);
- } else if (isOpen) setIsOpen(false);
-
- return (
- <>
-
- {label}
- {!hideDropdownArrow && !disabled && }
+
+
+
+
+
person.name}
+ onChange={(event) => setQuery(event.target.value)}
+ />
+
+
-
-
-
-
- setQuery(e.target.value)}
- placeholder="Search"
- displayValue={(assigned: any) => assigned?.name}
- />
+
+ setQuery('')}
+ >
+
+ {filteredPeople.length === 0 && query !== '' ? (
+
+ Nothing found.
-
- {filteredOptions ? (
- filteredOptions.length > 0 ? (
- filteredOptions.map((option) => (
-
- `flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
- active && !selected ? "bg-custom-background-80" : ""
- } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
- }
+ ) : (
+ filteredPeople.map((person) => (
+
+ `relative cursor-default select-none py-2 pl-10 pr-4 ${
+ active ? 'bg-teal-600 text-white' : 'text-gray-900'
+ }`
+ }
+ value={person}
+ >
+ {({ selected, active }) => (
+ <>
+
- {({ selected }) => (
- <>
- {option.content}
- {selected && }
- >
- )}
-
- ))
- ) : (
-
- No matching results
-
- )
- ) : (
- Loading...
- )}
-
-
-
- >
- );
- }}
-
+ {person.name}
+
+ {selected ? (
+
+
+
+ ) : null}
+ >
+ )}
+
+ ))
+ )}
+
+
+
+
+
);
};
diff --git a/web/components/ui/tooltip.tsx b/web/components/ui/tooltip.tsx
index 3a4c5d71f..0f9163521 100644
--- a/web/components/ui/tooltip.tsx
+++ b/web/components/ui/tooltip.tsx
@@ -51,17 +51,11 @@ export const Tooltip: React.FC = ({
content={
{tooltipHeading && (
-
+
{tooltipHeading}
)}
diff --git a/web/store/issue.ts b/web/store/issue.ts
index a4374e8ad..13ced147e 100644
--- a/web/store/issue.ts
+++ b/web/store/issue.ts
@@ -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 } };
});
diff --git a/web/store/project.ts b/web/store/project.ts
index 75a3aab04..057337c4f 100644
--- a/web/store/project.ts
+++ b/web/store/project.ts
@@ -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;
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise;
fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise;
fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise;
fetchProjectMembers: (workspaceSlug: string, projectId: string) => Promise;
+ fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise;
addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise;
removeProjectFromFavorites: (workspaceSlug: string, projectId: string) => Promise;
@@ -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);