diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 0048a8a19..f1d748e10 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -130,7 +130,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => { {projectDetails?.is_deployed && deployUrl && ( diff --git a/web/components/issues/issue-layouts/index.ts b/web/components/issues/issue-layouts/index.ts index 066febb94..aa82dc686 100644 --- a/web/components/issues/issue-layouts/index.ts +++ b/web/components/issues/issue-layouts/index.ts @@ -10,4 +10,6 @@ export * from "./gantt"; export * from "./kanban"; export * from "./spreadsheet"; +export * from "./properties"; + export * from "./roots"; diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index f30453a44..bfaa117c6 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -2,7 +2,7 @@ import { Draggable } from "@hello-pangea/dnd"; // components import { KanBanProperties } from "./properties"; // types -import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types"; +import { IIssueDisplayProperties, IIssue } from "types"; interface IssueBlockProps { sub_group_id: string; @@ -18,27 +18,10 @@ interface IssueBlockProps { ) => void; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties; - states: IState[] | null; - labels: IIssueLabels[] | null; - members: IUserLite[] | null; - estimates: IEstimatePoint[] | null; } export const KanbanIssueBlock: React.FC = (props) => { - const { - sub_group_id, - columnId, - index, - issue, - isDragDisabled, - handleIssues, - quickActions, - displayProperties, - states, - labels, - members, - estimates, - } = props; + const { sub_group_id, columnId, index, issue, isDragDisabled, handleIssues, quickActions, displayProperties } = props; const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => { if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, "update"); @@ -82,10 +65,6 @@ export const KanbanIssueBlock: React.FC = (props) => { issue={issue} handleIssues={updateIssue} displayProperties={displayProperties} - states={states} - labels={labels} - members={members} - estimates={estimates} /> diff --git a/web/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/components/issues/issue-layouts/kanban/blocks-list.tsx index 7c6e55f81..b167df709 100644 --- a/web/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/web/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -1,6 +1,6 @@ // components import { KanbanIssueBlock } from "components/issues"; -import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types"; +import { IIssueDisplayProperties, IIssue } from "types"; interface IssueBlocksListProps { sub_group_id: string; @@ -15,26 +15,10 @@ interface IssueBlocksListProps { ) => void; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; displayProperties: IIssueDisplayProperties; - states: IState[] | null; - labels: IIssueLabels[] | null; - members: IUserLite[] | null; - estimates: IEstimatePoint[] | null; } export const KanbanIssueBlocksList: React.FC = (props) => { - const { - sub_group_id, - columnId, - issues, - isDragDisabled, - handleIssues, - quickActions, - displayProperties, - states, - labels, - members, - estimates, - } = props; + const { sub_group_id, columnId, issues, isDragDisabled, handleIssues, quickActions, displayProperties } = props; return ( <> @@ -51,10 +35,6 @@ export const KanbanIssueBlocksList: React.FC = (props) => columnId={columnId} sub_group_id={sub_group_id} isDragDisabled={isDragDisabled} - states={states} - labels={labels} - members={members} - estimates={estimates} /> ))} diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index f441d3e22..8f000e56f 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -7,7 +7,7 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; import { KanbanIssueBlocksList, BoardInlineCreateIssueForm } from "components/issues"; // types -import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types"; +import { IIssueDisplayProperties, IIssue } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue"; @@ -30,11 +30,6 @@ export interface IGroupByKanBan { kanBanToggle: any; handleKanBanToggle: any; enableQuickIssueCreate?: boolean; - states: IState[] | null; - labels: IIssueLabels[] | null; - members: IUserLite[] | null; - priorities: any; - estimates: IEstimatePoint[] | null; } const GroupByKanBan: React.FC = observer((props) => { @@ -51,11 +46,6 @@ const GroupByKanBan: React.FC = observer((props) => { displayProperties, kanBanToggle, handleKanBanToggle, - states, - labels, - members, - priorities, - estimates, enableQuickIssueCreate, } = props; @@ -105,10 +95,6 @@ const GroupByKanBan: React.FC = observer((props) => { handleIssues={handleIssues} quickActions={quickActions} displayProperties={displayProperties} - states={states} - labels={labels} - members={members} - estimates={estimates} /> ) : ( isDragDisabled && ( @@ -154,13 +140,6 @@ export interface IKanBan { displayProperties: IIssueDisplayProperties; kanBanToggle: any; handleKanBanToggle: any; - states: IState[] | null; - stateGroups: any; - priorities: any; - labels: IIssueLabels[] | null; - members: IUserLite[] | null; - projects: IProject[] | null; - estimates: IEstimatePoint[] | null; enableQuickIssueCreate?: boolean; } @@ -175,13 +154,6 @@ export const KanBan: React.FC = observer((props) => { displayProperties, kanBanToggle, handleKanBanToggle, - states, - stateGroups, - priorities, - labels, - members, - projects, - estimates, enableQuickIssueCreate, } = props; @@ -204,11 +176,6 @@ export const KanBan: React.FC = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} - states={states} - labels={labels} - members={members} - priorities={priorities} - estimates={estimates} /> )} @@ -227,11 +194,6 @@ export const KanBan: React.FC = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} - states={states} - labels={labels} - members={members} - priorities={priorities} - estimates={estimates} /> )} @@ -250,11 +212,6 @@ export const KanBan: React.FC = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} - states={states} - labels={labels} - members={members} - priorities={priorities} - estimates={estimates} /> )} @@ -273,11 +230,6 @@ export const KanBan: React.FC = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} - states={states} - labels={labels} - members={members} - priorities={priorities} - estimates={estimates} /> )} @@ -296,11 +248,6 @@ export const KanBan: React.FC = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} - states={states} - labels={labels} - members={members} - priorities={priorities} - estimates={estimates} /> )} @@ -319,11 +266,6 @@ export const KanBan: React.FC = observer((props) => { kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} enableQuickIssueCreate={enableQuickIssueCreate} - states={states} - labels={labels} - members={members} - priorities={priorities} - estimates={estimates} /> )} diff --git a/web/components/issues/issue-layouts/kanban/properties.tsx b/web/components/issues/issue-layouts/kanban/properties.tsx index 10423973d..13b0e35b2 100644 --- a/web/components/issues/issue-layouts/kanban/properties.tsx +++ b/web/components/issues/issue-layouts/kanban/properties.tsx @@ -10,15 +10,7 @@ import { IssuePropertyAssignee } from "../properties/assignee"; import { IssuePropertyEstimates } from "../properties/estimates"; import { IssuePropertyDate } from "../properties/date"; import { Tooltip } from "@plane/ui"; -import { - IEstimatePoint, - IIssue, - IIssueDisplayProperties, - IIssueLabels, - IState, - IUserLite, - TIssuePriorities, -} from "types"; +import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types"; export interface IKanBanProperties { sub_group_id: string; @@ -26,24 +18,10 @@ export interface IKanBanProperties { issue: IIssue; handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => void; displayProperties: IIssueDisplayProperties; - states: IState[] | null; - labels: IIssueLabels[] | null; - members: IUserLite[] | null; - estimates: IEstimatePoint[] | null; } export const KanBanProperties: React.FC = observer((props) => { - const { - sub_group_id, - columnId: group_id, - issue, - handleIssues, - displayProperties, - states, - labels, - members, - estimates, - } = props; + const { sub_group_id, columnId: group_id, issue, handleIssues, displayProperties } = props; const handleState = (state: IState) => { handleIssues( @@ -107,9 +85,9 @@ export const KanBanProperties: React.FC = observer((props) => {/* state */} {displayProperties && displayProperties?.state && ( @@ -128,9 +106,9 @@ export const KanBanProperties: React.FC = observer((props) => {/* label */} {displayProperties && displayProperties?.labels && ( @@ -139,10 +117,10 @@ export const KanBanProperties: React.FC = observer((props) => {/* assignee */} {displayProperties && displayProperties?.assignee && ( )} @@ -170,9 +148,9 @@ export const KanBanProperties: React.FC = observer((props) => {/* estimates */} {displayProperties && displayProperties?.estimate && ( diff --git a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index ed54b0213..7eaeabf6a 100644 --- a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -116,13 +116,6 @@ export const CycleKanBanLayout: React.FC = observer(() => { displayProperties={displayProperties} kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} /> ) : ( { displayProperties={displayProperties} kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} /> ) : ( { displayProperties={displayProperties} kanBanToggle={issueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - estimates={null} /> ) : ( { displayProperties={displayProperties} kanBanToggle={issueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members?.map((m) => m.member) ?? null} - projects={projects} - enableQuickIssueCreate - estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} /> ) : ( = observer((props) => { displayProperties={displayProperties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={members} - projects={projects} - estimates={estimates} enableQuickIssueCreate /> diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index c697da78c..a7d24a6e8 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -4,7 +4,7 @@ import { IssuePeekOverview } from "components/issues/issue-peek-overview"; // ui import { Tooltip } from "@plane/ui"; // types -import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types"; +import { IIssue } from "types"; interface IssueBlockProps { columnId: string; @@ -12,14 +12,10 @@ interface IssueBlockProps { handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; - states: IState[] | null; - labels: IIssueLabels[] | null; - members: IUserLite[] | null; - estimates: IEstimatePoint[] | null; } export const IssueBlock: React.FC = (props) => { - const { columnId, issue, handleIssues, quickActions, display_properties, states, labels, members, estimates } = props; + const { columnId, issue, handleIssues, quickActions, display_properties } = props; const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => { handleIssues(group_by, issueToUpdate, "update"); @@ -54,10 +50,6 @@ export const IssueBlock: React.FC = (props) => { issue={issue} handleIssues={updateIssue} display_properties={display_properties} - states={states} - labels={labels} - members={members} - estimates={estimates} /> {quickActions(!columnId && columnId === "null" ? null : columnId, issue)} diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index 2350fe12c..31bba3bed 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; // components import { IssueBlock } from "components/issues"; // types -import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types"; +import { IIssue } from "types"; interface Props { columnId: string; @@ -10,15 +10,10 @@ interface Props { handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; - states: IState[] | null; - labels: IIssueLabels[] | null; - members: IUserLite[] | null; - estimates: IEstimatePoint[] | null; } export const IssueBlocksList: FC = (props) => { - const { columnId, issues, handleIssues, quickActions, display_properties, states, labels, members, estimates } = - props; + const { columnId, issues, handleIssues, quickActions, display_properties } = props; return (
@@ -31,10 +26,6 @@ export const IssueBlocksList: FC = (props) => { handleIssues={handleIssues} quickActions={quickActions} display_properties={display_properties} - states={states} - labels={labels} - members={members} - estimates={estimates} /> )) ) : ( diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index cb0fe6fff..83e4a11e0 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -17,14 +17,7 @@ export interface IGroupByList { quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; is_list?: boolean; - states: IState[] | null; - labels: IIssueLabels[] | null; - members: IUserLite[] | null; - projects: IProject[] | null; - stateGroups: any; - priorities: any; enableQuickIssueCreate?: boolean; - estimates: IEstimatePoint[] | null; } const GroupByList: React.FC = observer((props) => { @@ -37,13 +30,6 @@ const GroupByList: React.FC = observer((props) => { quickActions, display_properties, is_list = false, - states, - labels, - members, - projects, - stateGroups, - priorities, - estimates, enableQuickIssueCreate, } = props; @@ -70,10 +56,6 @@ const GroupByList: React.FC = observer((props) => { handleIssues={handleIssues} quickActions={quickActions} display_properties={display_properties} - states={states} - labels={labels} - members={members} - estimates={estimates} /> )} {enableQuickIssueCreate && ( @@ -121,7 +103,7 @@ export const List: React.FC = observer((props) => { projects, stateGroups, priorities, - estimates, + enableQuickIssueCreate, } = props; @@ -137,13 +119,6 @@ export const List: React.FC = observer((props) => { quickActions={quickActions} display_properties={display_properties} is_list - states={states} - labels={labels} - members={members} - projects={projects} - stateGroups={stateGroups} - priorities={priorities} - estimates={estimates} enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -157,13 +132,6 @@ export const List: React.FC = observer((props) => { handleIssues={handleIssues} quickActions={quickActions} display_properties={display_properties} - states={states} - labels={labels} - members={members} - projects={projects} - stateGroups={stateGroups} - priorities={priorities} - estimates={estimates} enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -177,13 +145,6 @@ export const List: React.FC = observer((props) => { handleIssues={handleIssues} quickActions={quickActions} display_properties={display_properties} - states={states} - labels={labels} - members={members} - projects={projects} - stateGroups={stateGroups} - priorities={priorities} - estimates={estimates} enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -197,13 +158,6 @@ export const List: React.FC = observer((props) => { handleIssues={handleIssues} quickActions={quickActions} display_properties={display_properties} - states={states} - labels={labels} - members={members} - projects={projects} - stateGroups={stateGroups} - priorities={priorities} - estimates={estimates} enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -217,13 +171,6 @@ export const List: React.FC = observer((props) => { handleIssues={handleIssues} quickActions={quickActions} display_properties={display_properties} - states={states} - labels={labels} - members={members} - projects={projects} - stateGroups={stateGroups} - priorities={priorities} - estimates={estimates} enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -237,13 +184,6 @@ export const List: React.FC = observer((props) => { handleIssues={handleIssues} quickActions={quickActions} display_properties={display_properties} - states={states} - labels={labels} - members={members} - projects={projects} - stateGroups={stateGroups} - priorities={priorities} - estimates={estimates} enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -257,13 +197,6 @@ export const List: React.FC = observer((props) => { handleIssues={handleIssues} quickActions={quickActions} display_properties={display_properties} - states={states} - labels={labels} - members={members} - projects={projects} - stateGroups={stateGroups} - priorities={priorities} - estimates={estimates} enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -277,13 +210,6 @@ export const List: React.FC = observer((props) => { handleIssues={handleIssues} quickActions={quickActions} display_properties={display_properties} - states={states} - labels={labels} - members={members} - projects={projects} - stateGroups={stateGroups} - priorities={priorities} - estimates={estimates} enableQuickIssueCreate={enableQuickIssueCreate} /> )} diff --git a/web/components/issues/issue-layouts/list/properties.tsx b/web/components/issues/issue-layouts/list/properties.tsx index 9da637dca..7e624f807 100644 --- a/web/components/issues/issue-layouts/list/properties.tsx +++ b/web/components/issues/issue-layouts/list/properties.tsx @@ -11,21 +11,17 @@ import { IssuePropertyDate } from "../properties/date"; // ui import { Tooltip } from "@plane/ui"; // types -import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite, TIssuePriorities } from "types"; +import { IIssue, IState, TIssuePriorities } from "types"; export interface IKanBanProperties { columnId: string; issue: IIssue; handleIssues: (group_by: string | null, issue: IIssue) => void; display_properties: any; - states: IState[] | null; - labels: IIssueLabels[] | null; - members: IUserLite[] | null; - estimates: IEstimatePoint[] | null; } export const KanBanProperties: FC = observer((props) => { - const { columnId: group_id, issue, handleIssues, display_properties, states, labels, members, estimates } = props; + const { columnId: group_id, issue, handleIssues, display_properties } = props; const handleState = (state: IState) => { handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: state.id }); @@ -59,13 +55,13 @@ export const KanBanProperties: FC = observer((props) => {
{/* basic properties */} {/* state */} - {display_properties && display_properties?.state && states && ( + {display_properties && display_properties?.state && ( )} @@ -80,24 +76,24 @@ export const KanBanProperties: FC = observer((props) => { )} {/* label */} - {display_properties && display_properties?.labels && labels && ( + {display_properties && display_properties?.labels && ( )} {/* assignee */} - {display_properties && display_properties?.assignee && members && ( + {display_properties && display_properties?.assignee && ( )} @@ -124,8 +120,8 @@ export const KanBanProperties: FC = observer((props) => { {/* estimates */} {display_properties && display_properties?.estimate && ( void; - members: IUserLite[] | null; disabled?: boolean; hideDropdownArrow?: boolean; + className?: string; + buttonClassName?: string; + optionsClassName?: string; + placement?: Placement; + multiple?: true; } export const IssuePropertyAssignee: React.FC = observer((props) => { - const { value, onChange, members, disabled = false, hideDropdownArrow = false } = props; + const { + view, + projectId, + value, + onChange, + disabled = false, + hideDropdownArrow = false, + className, + buttonClassName, + optionsClassName, + placement, + multiple = false, + } = props; + + const { workspace: workspaceStore, project: projectStore }: RootStore = useMobxStore(); + const workspaceSlug = workspaceStore?.workspaceSlug; + + const [query, setQuery] = useState(""); + + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + + const projectMembers = projectId && projectStore?.members?.[projectId]; + + const fetchProjectMembers = () => + workspaceSlug && projectId && projectStore.fetchProjectMembers(workspaceSlug, projectId); + + const options = (projectMembers ? projectMembers : [])?.map((member) => ({ + value: member.member.id, + query: member.member.display_name, + content: ( +
+ + {member.member.display_name} +
+ ), + })); + + const filteredOptions = + query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); + + const label = ( + 0 + ? (projectMembers ? projectMembers : []) + ?.filter((m) => value.includes(m.member.display_name)) + .map((m) => m.member.display_name) + .join(", ") + : "No Assignee" + } + position="top" + > +
+ {value && value.length > 0 && Array.isArray(value) ? ( + + ) : ( + + + + )} +
+
+ ); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const comboboxProps: any = { value, onChange, disabled }; + if (multiple) comboboxProps.multiple = true; return ( - + + + + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {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"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+
+
+
); }); diff --git a/web/components/issues/issue-layouts/properties/date.tsx b/web/components/issues/issue-layouts/properties/date.tsx index dbcbc0eac..f39741e19 100644 --- a/web/components/issues/issue-layouts/properties/date.tsx +++ b/web/components/issues/issue-layouts/properties/date.tsx @@ -42,7 +42,7 @@ export const IssuePropertyDate: React.FC = observer((props) <> diff --git a/web/components/issues/issue-layouts/properties/estimates.tsx b/web/components/issues/issue-layouts/properties/estimates.tsx index 83de934cb..907946ca7 100644 --- a/web/components/issues/issue-layouts/properties/estimates.tsx +++ b/web/components/issues/issue-layouts/properties/estimates.tsx @@ -1,28 +1,168 @@ +import { Fragment, useState } from "react"; + import { observer } from "mobx-react-lite"; -// components -import { EstimateSelect } from "components/estimates"; + +// hooks +import { usePopper } from "react-popper"; +import useEstimateOption from "hooks/use-estimate-option"; +// ui +import { Check, ChevronDown, Search, Triangle } from "lucide-react"; +import { Combobox } from "@headlessui/react"; +import { Tooltip } from "@plane/ui"; // types -import { IEstimatePoint } from "types"; +import { Placement } from "@popperjs/core"; export interface IIssuePropertyEstimates { + view?: "profile" | "workspace" | "project"; + projectId: string | null; value: number | null; onChange: (value: number | null) => void; - estimatePoints: IEstimatePoint[] | null; disabled?: boolean; hideDropdownArrow?: boolean; + className?: string; + buttonClassName?: string; + optionsClassName?: string; + placement?: Placement; } export const IssuePropertyEstimates: React.FC = observer((props) => { - const { value, onChange, estimatePoints, disabled, hideDropdownArrow = false } = props; + const { + view, + projectId, + value, + onChange, + disabled, + hideDropdownArrow = false, + className = "", + buttonClassName = "", + optionsClassName = "", + placement, + } = props; + + const [query, setQuery] = useState(""); + + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + + const { isEstimateActive, estimatePoints } = useEstimateOption(); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const options: { value: number | null; query: string; content: any }[] | undefined = (estimatePoints ?? []).map( + (estimate) => ({ + value: estimate.key, + query: estimate.value, + content: ( +
+ + {estimate.value} +
+ ), + }) + ); + options?.unshift({ + value: null, + query: "none", + content: ( +
+ + None +
+ ), + }); + + const filteredOptions = + query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); + + const selectedEstimate = estimatePoints?.find((e) => e.key === value); + const label = ( + +
+ + {selectedEstimate?.value ?? "None"} +
+
+ ); return ( - onChange(val as number | null)} disabled={disabled} - hideDropdownArrow={hideDropdownArrow} - /> + > + + + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {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 ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+
+
+ ); }); diff --git a/web/components/issues/issue-layouts/properties/index.tsx b/web/components/issues/issue-layouts/properties/index.tsx new file mode 100644 index 000000000..3e2e2acd6 --- /dev/null +++ b/web/components/issues/issue-layouts/properties/index.tsx @@ -0,0 +1,6 @@ +export * from "./assignee"; +export * from "./date"; +export * from "./estimates"; +export * from "./labels"; +export * from "./priority"; +export * from "./state"; diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx index a623d26cd..3caa4d779 100644 --- a/web/components/issues/issue-layouts/properties/labels.tsx +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -1,28 +1,212 @@ +import { Fragment, useState } from "react"; + import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; + +// hooks +import { usePopper } from "react-popper"; // components -import { LabelSelect } from "components/labels"; +import { Combobox } from "@headlessui/react"; +import { Tooltip } from "@plane/ui"; +import { Check, ChevronDown, Search } from "lucide-react"; // types -import { IIssueLabels } from "types"; +import { Placement } from "@popperjs/core"; +import { RootStore } from "store/root"; export interface IIssuePropertyLabels { + view?: "profile" | "workspace" | "project"; + projectId: string | null; value: string[]; onChange: (data: string[]) => void; - labels: IIssueLabels[] | null; disabled?: boolean; hideDropdownArrow?: boolean; + className?: string; + buttonClassName?: string; + optionsClassName?: string; + placement?: Placement; + maxRender?: number; } export const IssuePropertyLabels: React.FC = observer((props) => { - const { value, onChange, labels, disabled, hideDropdownArrow = false } = props; + const { + view, + projectId, + value, + onChange, + disabled, + hideDropdownArrow = false, + className, + buttonClassName, + optionsClassName, + placement, + maxRender = 2, + } = props; + + const { workspace: workspaceStore, project: projectStore }: RootStore = useMobxStore(); + const workspaceSlug = workspaceStore?.workspaceSlug; + + const [query, setQuery] = useState(""); + + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + + const projectLabels = projectId && projectStore?.labels?.[projectId]; + + const fetchProjectLabels = () => + workspaceSlug && projectId && projectStore.fetchProjectLabels(workspaceSlug, projectId); + + const options = (projectLabels ? projectLabels : []).map((label) => ({ + value: label.id, + query: label.name, + content: ( +
+ + {label.name} +
+ ), + })); + + const filteredOptions = + query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); return ( - + multiple + > + + + + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {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 ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+
+
+ ); }); diff --git a/web/components/issues/issue-layouts/properties/state.tsx b/web/components/issues/issue-layouts/properties/state.tsx index 9264b3084..ea23ccfcc 100644 --- a/web/components/issues/issue-layouts/properties/state.tsx +++ b/web/components/issues/issue-layouts/properties/state.tsx @@ -1,28 +1,175 @@ +import { Fragment, useState } from "react"; + import { observer } from "mobx-react-lite"; -// components -import { StateSelect } from "components/states"; +import { useMobxStore } from "lib/mobx/store-provider"; + +// hooks +import { usePopper } from "react-popper"; +// ui +import { Combobox } from "@headlessui/react"; +import { StateGroupIcon, Tooltip } from "@plane/ui"; +import { Check, ChevronDown, Search } from "lucide-react"; // types import { IState } from "types"; +import { Placement } from "@popperjs/core"; +import { RootStore } from "store/root"; export interface IIssuePropertyState { + view?: "profile" | "workspace" | "project"; + projectId: string | null; value: IState; onChange: (state: IState) => void; - states: IState[] | null; disabled?: boolean; hideDropdownArrow?: boolean; + className?: string; + buttonClassName?: string; + optionsClassName?: string; + placement?: Placement; } export const IssuePropertyState: React.FC = observer((props) => { - const { value, onChange, states, disabled, hideDropdownArrow = false } = props; + const { + view, + projectId, + value, + onChange, + disabled, + hideDropdownArrow = false, + className, + buttonClassName, + optionsClassName, + placement, + } = props; + + const { workspace: workspaceStore, project: projectStore }: RootStore = useMobxStore(); + const workspaceSlug = workspaceStore?.workspaceSlug; + + const [query, setQuery] = useState(""); + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + + const projectStates: IState[] = []; + const projectStatesByGroup = projectId && projectStore?.states?.[projectId]; + if (projectStatesByGroup) + for (const group in projectStatesByGroup) projectStates.push(...projectStatesByGroup[group]); + + const fetchProjectStates = () => + workspaceSlug && projectId && projectStore.fetchProjectStates(workspaceSlug, projectId); + + const dropdownOptions = projectStates?.map((state) => ({ + value: state.id, + query: state.name, + content: ( +
+ + {state.name} +
+ ), + })); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const filteredOptions = + query === "" + ? dropdownOptions + : dropdownOptions?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); + + const label = ( + +
+ {value && } + {value?.name ?? "State"} +
+
+ ); return ( - + <> + {workspaceSlug && projectId && ( + { + const selectedState = projectStates?.find((state) => state.id === data); + if (selectedState) onChange(selectedState); + }} + disabled={disabled} + > + + + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {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 ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ )} + ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx index db06d81b8..e0e855800 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx @@ -1,7 +1,7 @@ import React from "react"; // components -import { MembersSelect } from "components/project"; +import { IssuePropertyAssignee } from "../../properties"; // hooks import useSubIssue from "hooks/use-sub-issue"; // types @@ -21,11 +21,11 @@ export const SpreadsheetAssigneeColumn: React.FC = ({ issue, members, onC const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded); return ( -
- + onChange({ assignees: data })} - members={members ?? []} buttonClassName="!p-0 !rounded-none !border-0" hideDropdownArrow disabled={disabled} @@ -46,6 +46,6 @@ export const SpreadsheetAssigneeColumn: React.FC = ({ issue, members, onC disabled={disabled} /> ))} -
+ ); }; diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx index fa1cd4c0e..735e8908f 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx @@ -1,5 +1,5 @@ // components -import { EstimateSelect } from "components/estimates"; +import { IssuePropertyEstimates } from "../../properties"; // hooks import useSubIssue from "hooks/use-sub-issue"; // types @@ -21,14 +21,12 @@ export const SpreadsheetEstimateColumn: React.FC = (props) => { return ( <> - onChange({ estimate_point: data })} - className="h-full" - buttonClassName="!border-0 !h-full !w-full !rounded-none px-4" - estimatePoints={undefined} - disabled={disabled} hideDropdownArrow + disabled={disabled} /> {isExpanded && diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx index eaa6dab98..92400a033 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx @@ -1,7 +1,7 @@ import React from "react"; // components -import { LabelSelect } from "components/labels"; +import { IssuePropertyLabels } from "../../properties"; // hooks import useSubIssue from "hooks/use-sub-issue"; // types @@ -24,10 +24,10 @@ export const SpreadsheetLabelColumn: React.FC = (props) => { return ( <> - onChange({ labels: data })} - labels={labels ?? []} className="h-full" buttonClassName="!border-0 !h-full !w-full !rounded-none" hideDropdownArrow diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx index 1a9a32e2e..932d4359c 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx @@ -1,11 +1,9 @@ import React from "react"; // components -import { StateSelect } from "components/states"; +import { IssuePropertyState } from "../../properties"; // hooks import useSubIssue from "hooks/use-sub-issue"; -// helpers -import { getStatesList } from "helpers/state.helper"; // types import { IIssue, IStateResponse } from "types"; @@ -24,16 +22,13 @@ export const SpreadsheetStateColumn: React.FC = (props) => { const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded); - const statesList = getStatesList(states); - return ( <> - onChange({ state: data.id, state_detail: data })} - states={statesList} - className="h-full" - buttonClassName="!border-0 !h-full !w-full !rounded-none" + buttonClassName="!shadow-none !border-0" hideDropdownArrow disabled={disabled} /> diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index b4fb36383..be6bbb8e1 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -83,7 +83,7 @@ export const SpreadsheetView: React.FC = observer((props) => { ref={containerRef} className="flex max-h-full h-full overflow-y-auto divide-x-[0.5px] divide-custom-border-200" > - {issues ? ( + {issues && issues.length > 0 ? ( <>
= (props) => { issueCommentReactionRemove, } = props; - console.log("issueComments", issueComments); - return (
    diff --git a/web/components/issues/issue-peek-overview/activity/view.tsx b/web/components/issues/issue-peek-overview/activity/view.tsx index d7f9bcf92..e50d4e827 100644 --- a/web/components/issues/issue-peek-overview/activity/view.tsx +++ b/web/components/issues/issue-peek-overview/activity/view.tsx @@ -36,8 +36,8 @@ export const IssueComment: FC = (props) => { }; return ( -
    -
    Activity
    +
    +
    Activity
    = (props) => { const { workspaceSlug, issue, issueReactions, user, issueUpdate, issueReactionCreate, issueReactionRemove } = props; + const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); - const { handleSubmit, watch, reset, control } = useForm({ + const [characterLimit, setCharacterLimit] = useState(false); + + const { setShowAlert } = useReloadConfirmations(); + const { + handleSubmit, + watch, + reset, + control, + formState: { errors }, + } = useForm({ defaultValues: { name: "", description_html: "", }, }); - const { setShowAlert } = useReloadConfirmations(); + const handleDescriptionFormSubmit = useCallback( + async (formData: Partial) => { + if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; - useEffect(() => { - if (!issue) return; + await issueUpdate({ + ...issue, + name: formData.name ?? "", + description_html: formData.description_html ?? "

    ", + }); + }, + [issue, issueUpdate] + ); - reset({ - ...issue, - }); - }, [issue, reset]); + const debouncedIssueDescription = useDebouncedCallback(async (_data: any) => { + issueUpdate({ ...issue, description_html: _data }); + }, 1500); + + const debouncedTitleSave = useDebouncedCallback(async () => { + handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted")); + }, 1500); useEffect(() => { if (isSubmitting === "submitted") { @@ -56,62 +78,80 @@ export const PeekOverviewIssueDetails: FC = (props) = } }, [isSubmitting, setShowAlert]); - const handleDescriptionFormSubmit = useCallback( - async (formData: Partial) => { - if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; + // reset form values + useEffect(() => { + if (!issue) return; - issueUpdate({ name: formData.name ?? "", description_html: formData.description_html }); - }, - [issueUpdate] - ); - - const debouncedIssueFormSave = useDebouncedCallback(async () => { - handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted")); - }, 1500); + reset({ + ...issue, + }); + }, [issue, reset]); return ( -
    -
    + <> + {issue?.project_detail?.identifier}-{issue?.sequence_id} -
    + -
    {watch("name")}
    - -
    -
    +
    + {true ? ( ( - { + placeholder="Enter issue name" + onFocus={() => setCharacterLimit(true)} + onChange={(e: ChangeEvent) => { + setCharacterLimit(false); setIsSubmitting("submitting"); - onChange(description_html); - debouncedIssueFormSave(); + debouncedTitleSave(); + onChange(e.target.value); }} - customClassName="p-3 min-h-[80px] shadow-sm" + required={true} + className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary !p-0 focus:!px-3 focus:!py-2" + hasError={Boolean(errors?.description)} + role="textbox" + disabled={!true} /> )} /> -
    - {isSubmitting === "submitting" ? "Saving..." : "Saved"} + ) : ( +

    {issue.name}

    + )} + {characterLimit && true && ( +
    + 255 ? "text-red-500" : ""}`}> + {watch("name").length} + + /255
    -
    - - + )}
    -
    + {errors.name ? errors.name.message : null} + + + { + debouncedIssueDescription(description_html); + }} + customClassName="mt-0" + /> + + + + ); }; diff --git a/web/components/issues/issue-peek-overview/properties.tsx b/web/components/issues/issue-peek-overview/properties.tsx index 14ba0c57d..b336beeec 100644 --- a/web/components/issues/issue-peek-overview/properties.tsx +++ b/web/components/issues/issue-peek-overview/properties.tsx @@ -1,139 +1,358 @@ -import { FC } from "react"; +import { FC, useState } from "react"; +import { mutate } from "swr"; +import { useRouter } from "next/router"; + // ui icons -import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; -import { CalendarDays, Signal } from "lucide-react"; +import { DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; +import { CalendarDays, ContrastIcon, Link2, Plus, Signal, Tag, Triangle, User2 } from "lucide-react"; +import { + SidebarAssigneeSelect, + SidebarCycleSelect, + SidebarEstimateSelect, + SidebarLabelSelect, + SidebarModuleSelect, + SidebarParentSelect, + SidebarPrioritySelect, + SidebarStateSelect, +} from "../sidebar-select"; +// hooks +import useToast from "hooks/use-toast"; + // components -import { IssuePropertyState } from "components/issues/issue-layouts/properties/state"; -import { IssuePropertyPriority } from "components/issues/issue-layouts/properties/priority"; -import { IssuePropertyAssignee } from "components/issues/issue-layouts/properties/assignee"; -import { IssuePropertyDate } from "components/issues/issue-layouts/properties/date"; +import { CustomDatePicker } from "components/ui"; +import { LinkModal, LinksList } from "components/core"; // types -import { IIssue, IState, IUserLite, TIssuePriorities } from "types"; +import { ICycle, IIssue, IIssueLink, IModule, TIssuePriorities, linkDetails } from "types"; +// contexts +import { useProjectMyMembership } from "contexts/project-member.context"; +import { ISSUE_DETAILS } from "constants/fetch-keys"; + +// services +import { IssueService } from "services/issue"; interface IPeekOverviewProperties { issue: IIssue; issueUpdate: (issue: Partial) => void; - states: IState[] | null; - members: IUserLite[] | null; - priorities: any; + user: any; } +const issueService = new IssueService(); + export const PeekOverviewProperties: FC = (props) => { - const { issue, issueUpdate, states, members, priorities } = props; + const { issue, issueUpdate, user } = props; + const [linkModal, setLinkModal] = useState(false); + const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); - const handleState = (_state: IState) => { - issueUpdate({ ...issue, state: _state.id }); + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { setToastAlert } = useToast(); + + const { memberRole } = useProjectMyMembership(); + + const handleState = (_state: string) => { + issueUpdate({ ...issue, state: _state }); }; - const handlePriority = (_priority: TIssuePriorities) => { issueUpdate({ ...issue, priority: _priority }); }; - const handleAssignee = (_assignees: string[]) => { issueUpdate({ ...issue, assignees: _assignees }); }; - - const handleStartDate = (_startDate: string) => { + const handleEstimate = (_estimate: number | null) => { + issueUpdate({ ...issue, estimate_point: _estimate }); + }; + const handleStartDate = (_startDate: string | null) => { issueUpdate({ ...issue, start_date: _startDate }); }; - - const handleTargetDate = (_targetDate: string) => { + const handleTargetDate = (_targetDate: string | null) => { issueUpdate({ ...issue, target_date: _targetDate }); }; + const handleParent = (_parent: string) => { + issueUpdate({ ...issue, parent: _parent }); + }; + const handleCycle = (_cycle: ICycle) => { + issueUpdate({ ...issue, cycle: _cycle.id }); + }; + const handleModule = (_module: IModule) => { + issueUpdate({ ...issue, module: _module.id }); + }; + const handleLabels = (formData: Partial) => { + issueUpdate({ ...issue, ...formData }); + }; + + const handleCreateLink = async (formData: IIssueLink) => { + if (!workspaceSlug || !projectId || !issue) return; + + const payload = { metadata: {}, ...formData }; + + await issueService + .createIssueLink(workspaceSlug as string, projectId as string, issue.id, payload) + .then(() => mutate(ISSUE_DETAILS(issue.id))) + .catch((err) => { + if (err.status === 400) + setToastAlert({ + type: "error", + title: "Error!", + message: "This URL already exists for this issue.", + }); + else + setToastAlert({ + type: "error", + title: "Error!", + message: "Something went wrong. Please try again.", + }); + }); + }; + + const handleUpdateLink = async (formData: IIssueLink, linkId: string) => { + if (!workspaceSlug || !projectId || !issue) return; + + const payload = { metadata: {}, ...formData }; + + const updatedLinks = issue.issue_link.map((l) => + l.id === linkId + ? { + ...l, + title: formData.title, + url: formData.url, + } + : l + ); + + mutate( + ISSUE_DETAILS(issue.id), + (prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }), + false + ); + + await issueService + .updateIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId, payload) + .then(() => { + mutate(ISSUE_DETAILS(issue.id)); + }) + .catch((err) => { + console.log(err); + }); + }; + + const handleEditLink = (link: linkDetails) => { + setSelectedLinkToUpdate(link); + setLinkModal(true); + }; + + const handleDeleteLink = async (linkId: string) => { + if (!workspaceSlug || !projectId || !issue) return; + + const updatedLinks = issue.issue_link.filter((l) => l.id !== linkId); + + mutate( + ISSUE_DETAILS(issue.id), + (prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }), + false + ); + + await issueService + .deleteIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId) + .then(() => { + mutate(ISSUE_DETAILS(issue.id)); + }) + .catch((err) => { + console.log(err); + }); + }; + + const minDate = issue.start_date ? new Date(issue.start_date) : null; + minDate?.setDate(minDate.getDate()); + + const maxDate = issue.target_date ? new Date(issue.target_date) : null; + maxDate?.setDate(maxDate.getDate()); + + const isNotAllowed = user?.memberRole?.isGuest || user?.memberRole?.isViewer; return ( -
    - {/* state */} -
    -
    -
    - + <> + { + setLinkModal(false); + setSelectedLinkToUpdate(null); + }} + data={selectedLinkToUpdate} + status={selectedLinkToUpdate ? true : false} + createIssueLink={handleCreateLink} + updateIssueLink={handleUpdateLink} + /> +
    +
    + {/* state */} +
    +
    + +

    State

    +
    +
    + +
    -
    State
    -
    -
    - -
    -
    - {/* assignees */} -
    -
    -
    - + {/* assignee */} +
    +
    + +

    Assignees

    +
    +
    + +
    -
    Assignees
    -
    -
    - handleAssignee(ids)} - disabled={false} - hideDropdownArrow - members={members} - /> -
    -
    - {/* priority */} -
    -
    -
    - + {/* priority */} +
    +
    + +

    Priority

    +
    +
    + +
    -
    Priority
    -
    -
    - -
    -
    - {/* start_date */} -
    -
    -
    - + {/* estimate */} +
    +
    + +

    Estimate

    +
    +
    + +
    -
    Start date
    -
    -
    - handleStartDate(date)} - disabled={false} - placeHolder={`Start date`} - /> -
    -
    - {/* target_date */} -
    -
    -
    - + {/* start date */} +
    +
    + +

    Start date

    +
    +
    + +
    +
    + + {/* due date */} +
    +
    + +

    Due date

    +
    +
    + +
    +
    + + {/* parent */} +
    +
    + +

    Parent

    +
    +
    + +
    -
    Target date
    -
    - handleTargetDate(date)} - disabled={false} - placeHolder={`Target date`} - /> + + + +
    +
    +
    + +

    Cycle

    +
    +
    + +
    +
    + +
    +
    + +

    Module

    +
    +
    + +
    +
    +
    +
    + +

    Label

    +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    + +

    Links

    +
    +
    + {!isNotAllowed && ( + + )} +
    +
    +
    + {issue?.issue_link && issue.issue_link.length > 0 ? ( + + ) : null} +
    +
    -
    + ); }; diff --git a/web/components/issues/issue-peek-overview/reactions/preview.tsx b/web/components/issues/issue-peek-overview/reactions/preview.tsx index bb9c6c4f7..46c1cc2ce 100644 --- a/web/components/issues/issue-peek-overview/reactions/preview.tsx +++ b/web/components/issues/issue-peek-overview/reactions/preview.tsx @@ -26,13 +26,13 @@ export const IssueReactionPreview: FC = (props) => { type="button" onClick={() => handleReaction(reaction)} key={reaction} - className={`flex items-center gap-1.5 text-custom-text-100 text-sm h-full px-2 py-1 rounded-md ${ + className={`flex items-center gap-1.5 text-custom-text-100 text-sm h-full px-2 py-1 rounded ${ isUserReacted(issueReactions[reaction]) - ? `bg-custom-primary-100/20 hover:bg-custom-primary-100/30` + ? `bg-custom-primary-100/10 hover:bg-custom-primary-100/30` : `bg-custom-background-90 hover:bg-custom-background-100/30` }`} > - {renderEmoji(reaction)} + {renderEmoji(reaction)} = (props) => { <> - - + + = observer((props) => { const { workspaceSlug, projectId, issueId, handleIssue, children } = props; - const { project: projectStore, issueDetail: issueDetailStore }: RootStore = useMobxStore(); - - const states = projectStore?.projectStates || undefined; - const members = projectStore?.projectMembers || undefined; - const priorities = ISSUE_PRIORITIES || undefined; + const { issueDetail: issueDetailStore }: RootStore = useMobxStore(); const issueUpdate = (_data: Partial) => { if (handleIssue) { @@ -55,14 +49,17 @@ export const IssuePeekOverview: FC = observer((props) => { const issueCommentReactionRemove = (commentId: string, reaction: string) => issueDetailStore.removeIssueCommentReaction(workspaceSlug, projectId, issueId, commentId, reaction); + const issueSubscriptionCreate = () => issueDetailStore.createIssueSubscription(workspaceSlug, projectId, issueId); + + const issueSubscriptionRemove = () => issueDetailStore.removeIssueSubscription(workspaceSlug, projectId, issueId); + + const handleDeleteIssue = () => issueDetailStore.deleteIssue(workspaceSlug, projectId, issueId); + return ( = observer((props) => { issueCommentRemove={issueCommentRemove} issueCommentReactionCreate={issueCommentReactionCreate} issueCommentReactionRemove={issueCommentReactionRemove} + issueSubscriptionCreate={issueSubscriptionCreate} + issueSubscriptionRemove={issueSubscriptionRemove} + handleDeleteIssue={handleDeleteIssue} > {children} diff --git a/web/components/issues/issue-peek-overview/view.tsx b/web/components/issues/issue-peek-overview/view.tsx index ca7ac2fc0..3f1b0765a 100644 --- a/web/components/issues/issue-peek-overview/view.tsx +++ b/web/components/issues/issue-peek-overview/view.tsx @@ -1,17 +1,22 @@ -import { FC, ReactNode, useEffect, useState } from "react"; +import { FC, ReactNode, useState } from "react"; import { useRouter } from "next/router"; -import { Maximize2, ArrowRight, Link, Trash, PanelRightOpen, Square, SquareCode } from "lucide-react"; +import { PanelRightOpen, Square, SquareCode, MoveRight, MoveDiagonal, Bell, Link2, Trash2 } from "lucide-react"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; // components import { PeekOverviewIssueDetails } from "./issue-detail"; import { PeekOverviewProperties } from "./properties"; import { IssueComment } from "./activity"; +import { Button, CustomSelect, FullScreenPeekIcon, ModalPeekIcon, SidePeekIcon } from "@plane/ui"; +import { DeleteIssueModal } from "../delete-issue-modal"; // types import { IIssue } from "types"; import { RootStore } from "store/root"; // hooks import { useMobxStore } from "lib/mobx/store-provider"; +import useToast from "hooks/use-toast"; +// helpers +import { copyUrlToClipboard } from "helpers/string.helper"; interface IIssueView { workspaceSlug: string; @@ -25,9 +30,9 @@ interface IIssueView { issueCommentRemove: (commentId: string) => void; issueCommentReactionCreate: (commentId: string, reaction: string) => void; issueCommentReactionRemove: (commentId: string, reaction: string) => void; - states: any; - members: any; - priorities: any; + issueSubscriptionCreate: () => void; + issueSubscriptionRemove: () => void; + handleDeleteIssue: () => Promise; children: ReactNode; } @@ -36,17 +41,17 @@ type TPeekModes = "side-peek" | "modal" | "full-screen"; const peekOptions: { key: TPeekModes; icon: any; title: string }[] = [ { key: "side-peek", - icon: PanelRightOpen, + icon: SidePeekIcon, title: "Side Peek", }, { key: "modal", - icon: Square, + icon: ModalPeekIcon, title: "Modal", }, { key: "full-screen", - icon: SquareCode, + icon: FullScreenPeekIcon, title: "Full Screen", }, ]; @@ -64,9 +69,9 @@ export const IssueView: FC = observer((props) => { issueCommentRemove, issueCommentReactionCreate, issueCommentReactionRemove, - states, - members, - priorities, + issueSubscriptionCreate, + issueSubscriptionRemove, + handleDeleteIssue, children, } = props; @@ -76,8 +81,20 @@ export const IssueView: FC = observer((props) => { const { user: userStore, issueDetail: issueDetailStore }: RootStore = useMobxStore(); const [peekMode, setPeekMode] = useState("side-peek"); - const handlePeekMode = (_peek: TPeekModes) => { - if (peekMode != _peek) setPeekMode(_peek); + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + + const { setToastAlert } = useToast(); + + const handleCopyText = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues/${peekIssueId}`).then(() => { + setToastAlert({ + type: "success", + title: "Link Copied!", + message: "Issue link copied to clipboard.", + }); + }); }; const updateRoutePeekId = () => { @@ -117,128 +134,129 @@ export const IssueView: FC = observer((props) => { } ); + useSWR( + workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId + ? `ISSUE_PEEK_OVERVIEW_SUBSCRIPTION_${workspaceSlug}_${projectId}_${peekIssueId}` + : null, + async () => { + if (workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId) { + await issueDetailStore.fetchIssueSubscription(workspaceSlug, projectId, issueId); + } + } + ); + const issue = issueDetailStore.getIssue; const issueReactions = issueDetailStore.getIssueReactions; const issueComments = issueDetailStore.getIssueComments; + const issueSubscription = issueDetailStore.getIssueSubscription; const user = userStore?.currentUser; - return ( -
    -
    - {children} -
    + const currentMode = peekOptions.find((m) => m.key === peekMode); - {issueId === peekIssueId && ( -
    + {issue && ( + setDeleteIssueModal(false)} + data={issue} + onSubmit={handleDeleteIssue} + /> + )} +
    +
    + {children} +
    + + {issueId === peekIssueId && ( +
    - {/* header */} -
    -
    - -
    + style={{ + boxShadow: + "0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12), 0px 1px 16px 0px rgba(16, 24, 40, 0.12)", + }} + > + {/* header */} +
    +
    + -
    - -
    + + {currentMode && ( +
    + setPeekMode(val)} + customButton={ + + } + > + {peekOptions.map((mode) => ( + +
    + + {mode.title} +
    +
    + ))} +
    +
    + )} +
    -
    - {peekOptions.map((_option) => ( -
    handlePeekMode(_option?.key)} +
    +
    - ))} -
    - -
    -
    - Subscribe -
    - -
    - -
    - -
    - + {issueSubscription && issueSubscription.subscribed ? "Unsubscribe" : "Subscribe"} + + +
    -
    - {/* content */} -
    - {issueDetailStore?.loader && !issue ? ( -
    Loading...
    - ) : ( - issue && ( - <> - {["side-peek", "modal"].includes(peekMode) ? ( -
    - - - - -
    - - -
    - ) : ( -
    -
    + {/* content */} +
    + {issueDetailStore?.loader && !issue ? ( +
    Loading...
    + ) : ( + issue && ( + <> + {["side-peek", "modal"].includes(peekMode) ? ( +
    -
    + = observer((props) => { issueCommentReactionRemove={issueCommentReactionRemove} />
    -
    - + ) : ( +
    +
    + + +
    + + +
    +
    + +
    -
    - )} - - ) - )} + )} + + ) + )} +
    -
    - )} -
    + )} +
    + ); }); diff --git a/web/components/issues/sidebar-select/estimate.tsx b/web/components/issues/sidebar-select/estimate.tsx index 79733131f..cdc74cbb2 100644 --- a/web/components/issues/sidebar-select/estimate.tsx +++ b/web/components/issues/sidebar-select/estimate.tsx @@ -16,13 +16,20 @@ type Props = { export const SidebarEstimateSelect: React.FC = ({ value, onChange, disabled = false }) => { const { estimatePoints } = useEstimateOption(); + const currentEstimate = estimatePoints?.find((e) => e.key === value)?.value; return ( - - {estimatePoints?.find((e) => e.key === value)?.value ?? "No estimate"} +
    + {currentEstimate ? ( + <> + + {currentEstimate} + + ) : ( + "No Estimate" + )}
    } onChange={onChange} @@ -31,7 +38,7 @@ export const SidebarEstimateSelect: React.FC = ({ value, onChange, disabl <> - + None @@ -41,7 +48,7 @@ export const SidebarEstimateSelect: React.FC = ({ value, onChange, disabl <> - + {point.value} diff --git a/web/components/issues/sidebar-select/label.tsx b/web/components/issues/sidebar-select/label.tsx index a442a1d42..4f9bdf5ea 100644 --- a/web/components/issues/sidebar-select/label.tsx +++ b/web/components/issues/sidebar-select/label.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; -import { Controller, UseFormWatch, useForm } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import { TwitterPicker } from "react-color"; // headless ui import { Listbox, Popover, Transition } from "@headlessui/react"; @@ -12,7 +12,7 @@ import useUser from "hooks/use-user"; // ui import { Input, Spinner } from "@plane/ui"; // icons -import { Component, Plus, Tag, X } from "lucide-react"; +import { Component, Plus, X } from "lucide-react"; // types import { IIssue, IIssueLabels } from "types"; // fetch-keys @@ -20,8 +20,7 @@ import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; type Props = { issueDetails: IIssue | undefined; - issueControl: any; - watchIssue: UseFormWatch; + labelList: string[]; submitChanges: (formData: any) => void; isNotAllowed: boolean; uneditable: boolean; @@ -36,8 +35,7 @@ const issueLabelService = new IssueLabelService(); export const SidebarLabelSelect: React.FC = ({ issueDetails, - issueControl, - watchIssue, + labelList, submitChanges, isNotAllowed, uneditable, @@ -91,167 +89,152 @@ export const SidebarLabelSelect: React.FC = ({ }, [createLabelForm, reset, setFocus]); return ( -
    -
    -
    - -

    Label

    -
    -
    -
    - {watchIssue("labels")?.map((labelId) => { - const label = issueLabels?.find((l) => l.id === labelId); +
    +
    + {labelList?.map((labelId) => { + const label = issueLabels?.find((l) => l.id === labelId); - if (label) - return ( - { - const updatedLabels = watchIssue("labels")?.filter((l) => l !== labelId); - submitChanges({ - labels: updatedLabels, - }); - }} - > - - {label.name} - - - ); - })} - ( - submitChanges({ labels: val })} - className="flex-shrink-0" - multiple - disabled={isNotAllowed || uneditable} - > - {({ open }) => ( -
    - - Select Label - - - - -
    - {issueLabels ? ( - issueLabels.length > 0 ? ( - issueLabels.map((label: IIssueLabels) => { - const children = issueLabels?.filter((l) => l.parent === label.id); - - if (children.length === 0) { - if (!label.parent) - return ( - - `${active || selected ? "bg-custom-background-90" : ""} ${ - selected ? "" : "text-custom-text-200" - } flex cursor-pointer select-none items-center gap-2 truncate p-2` - } - value={label.id} - > - - {label.name} - - ); - } else - return ( -
    -
    - - {label.name} -
    -
    - {children.map((child) => ( - - `${active || selected ? "bg-custom-background-100" : ""} ${ - selected ? "" : "text-custom-text-200" - } flex cursor-pointer select-none items-center gap-2 truncate p-2` - } - value={child.id} - > - - {child.name} - - ))} -
    -
    - ); - }) - ) : ( -
    No labels found
    - ) - ) : ( - - )} -
    -
    -
    -
    - )} -
    - )} - /> - {!isNotAllowed && ( - + Select Label + + + + +
    + {issueLabels ? ( + issueLabels.length > 0 ? ( + issueLabels.map((label: IIssueLabels) => { + const children = issueLabels?.filter((l) => l.parent === label.id); + + if (children.length === 0) { + if (!label.parent) + return ( + + `${active || selected ? "bg-custom-background-90" : ""} ${ + selected ? "" : "text-custom-text-200" + } flex cursor-pointer select-none items-center gap-2 truncate p-2` + } + value={label.id} + > + + {label.name} + + ); + } else + return ( +
    +
    + + {label.name} +
    +
    + {children.map((child) => ( + + `${active || selected ? "bg-custom-background-100" : ""} ${ + selected ? "" : "text-custom-text-200" + } flex cursor-pointer select-none items-center gap-2 truncate p-2` + } + value={child.id} + > + + {child.name} + + ))} +
    +
    + ); + }) + ) : ( +
    No labels found
    + ) + ) : ( + + )} +
    +
    +
    +
    + )} + + {!isNotAllowed && ( +
    -
    + + )}
    + {createLabelForm && (
    diff --git a/web/components/issues/sidebar.tsx b/web/components/issues/sidebar.tsx index d87033f5b..e8c4d2431 100644 --- a/web/components/issues/sidebar.tsx +++ b/web/components/issues/sidebar.tsx @@ -32,7 +32,7 @@ import { // ui import { CustomDatePicker } from "components/ui"; // icons -import { Bell, CalendarDays, LinkIcon, Plus, Signal, Trash2, Triangle, User2 } from "lucide-react"; +import { Bell, CalendarDays, LinkIcon, Plus, Signal, Tag, Trash2, Triangle, User2 } from "lucide-react"; import { ContrastIcon, DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; @@ -333,7 +333,7 @@ export const IssueDetailsSidebar: React.FC = ({

    State

    -
    +
    = ({

    Assignees

    -
    +
    = ({

    Priority

    -
    +
    = ({

    Cycle

    -
    +
    = ({

    Module

    -
    +
    = ({ )}
    {(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && ( - +
    +
    + +

    Label

    +
    +
    + +
    +
    )} {(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
    diff --git a/web/components/issues/sub-issues/properties.tsx b/web/components/issues/sub-issues/properties.tsx index 665ff8514..faf593efb 100644 --- a/web/components/issues/sub-issues/properties.tsx +++ b/web/components/issues/sub-issues/properties.tsx @@ -8,14 +8,12 @@ import { IssueService } from "services/issue"; import { TrackEventService } from "services/track_event.service"; // components import { ViewDueDateSelect, ViewStartDateSelect } from "components/issues"; -import { MembersSelect, PrioritySelect } from "components/project"; -import { StateSelect } from "components/states"; -// helpers -import { getStatesList } from "helpers/state.helper"; +import { PrioritySelect } from "components/project"; // types import { IUser, IIssue, IState } from "types"; // fetch-keys import { SUB_ISSUES } from "constants/fetch-keys"; +import { IssuePropertyAssignee, IssuePropertyState } from "../issue-layouts/properties"; export interface IIssueProperty { workspaceSlug: string; @@ -32,7 +30,7 @@ const trackEventService = new TrackEventService(); export const IssueProperty: React.FC = observer((props) => { const { workspaceSlug, parentIssue, issue, user, editable } = props; - const { project: projectStore, issueFilter: issueFilterStore } = useMobxStore(); + const { issueFilter: issueFilterStore } = useMobxStore(); const displayProperties = issueFilterStore.userDisplayProperties ?? {}; @@ -117,8 +115,6 @@ export const IssueProperty: React.FC = observer((props) => { ); }; - const statesList = getStatesList(projectStore.states?.[issue.project]); - return (
    {displayProperties.priority && ( @@ -134,12 +130,12 @@ export const IssueProperty: React.FC = observer((props) => { {displayProperties.state && (
    - handleStateChange(data)} - hideDropdownArrow - disabled={!editable} + disabled={false} + hideDropdownArrow={true} />
    )} @@ -168,13 +164,12 @@ export const IssueProperty: React.FC = observer((props) => { {displayProperties.assignee && (
    - handleAssigneeChange(val)} - members={projectStore.members ? (projectStore.members[issue.project] ?? []).map((m) => m.member) : []} - hideDropdownArrow - disabled={!editable} - multiple + disabled={false} />
    )} diff --git a/web/store/issue/issue_detail.store.ts b/web/store/issue/issue_detail.store.ts index 77f11d87b..58d4aabe7 100644 --- a/web/store/issue/issue_detail.store.ts +++ b/web/store/issue/issue_detail.store.ts @@ -1,6 +1,7 @@ import { observable, action, makeObservable, runInAction, computed } from "mobx"; // services import { IssueService, IssueReactionService, IssueCommentService } from "services/issue"; +import { NotificationService } from "services/notification.service"; // types import { RootStore } from "../root"; import { IIssue } from "types"; @@ -28,6 +29,9 @@ export interface IIssueDetailStore { [comment_id: string]: any; }; }; + issue_subscription: { + [issueId: string]: any; + }; setPeekId: (issueId: string | null) => void; @@ -36,6 +40,7 @@ export interface IIssueDetailStore { getIssueReactions: any | null; getIssueComments: any | null; getIssueCommentReactions: any | null; + getIssueSubscription: any | null; // fetch issue details fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -84,6 +89,10 @@ export interface IIssueDetailStore { commentId: string, reaction: string ) => Promise; + + fetchIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + createIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + removeIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise; } export class IssueDetailStore implements IIssueDetailStore { @@ -103,6 +112,9 @@ export class IssueDetailStore implements IIssueDetailStore { issue_comment_reactions: { [issueId: string]: any; } = {}; + issue_subscription: { + [issueId: string]: any; + } = {}; // root store rootStore; @@ -110,6 +122,7 @@ export class IssueDetailStore implements IIssueDetailStore { issueService; issueReactionService; issueCommentService; + notificationService; constructor(_rootStore: RootStore) { makeObservable(this, { @@ -122,11 +135,13 @@ export class IssueDetailStore implements IIssueDetailStore { issue_reactions: observable.ref, issue_comments: observable.ref, issue_comment_reactions: observable.ref, + issue_subscription: observable.ref, getIssue: computed, getIssueReactions: computed, getIssueComments: computed, getIssueCommentReactions: computed, + getIssueSubscription: computed, setPeekId: action, @@ -150,12 +165,17 @@ export class IssueDetailStore implements IIssueDetailStore { fetchIssueCommentReactions: action, creationIssueCommentReaction: action, removeIssueCommentReaction: action, + + fetchIssueSubscription: action, + createIssueSubscription: action, + removeIssueSubscription: action, }); this.rootStore = _rootStore; this.issueService = new IssueService(); this.issueReactionService = new IssueReactionService(); this.issueCommentService = new IssueCommentService(); + this.notificationService = new NotificationService(); } get getIssue() { @@ -182,6 +202,12 @@ export class IssueDetailStore implements IIssueDetailStore { return _commentReactions || null; } + get getIssueSubscription() { + if (!this.peekId) return null; + const _commentSubscription = this.issue_subscription[this.peekId]; + return _commentSubscription || null; + } + setPeekId = (issueId: string | null) => (this.peekId = issueId); fetchIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => { @@ -662,4 +688,61 @@ export class IssueDetailStore implements IIssueDetailStore { throw error; } }; + + // subscription + fetchIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + const _subscription = await this.notificationService.getIssueNotificationSubscriptionStatus( + workspaceSlug, + projectId, + issueId + ); + + const _issue_subscription = { + ...this.issue_subscription, + [issueId]: _subscription, + }; + + runInAction(() => { + this.issue_subscription = _issue_subscription; + }); + } catch (error) { + console.warn("error fetching the issue subscription", error); + throw error; + } + }; + createIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + await this.notificationService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId); + + const _issue_subscription = { + ...this.issue_subscription, + [issueId]: { subscribed: true }, + }; + + runInAction(() => { + this.issue_subscription = _issue_subscription; + }); + } catch (error) { + console.warn("error creating the issue subscription", error); + throw error; + } + }; + removeIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + const _issue_subscription = { + ...this.issue_subscription, + [issueId]: { subscribed: false }, + }; + + runInAction(() => { + this.issue_subscription = _issue_subscription; + }); + + await this.notificationService.unsubscribeFromIssueNotifications(workspaceSlug, projectId, issueId); + } catch (error) { + console.warn("error removing the issue subscription", error); + throw error; + } + }; }