[WEB-2907] chore: issue store updated and code refactor (#6279)

* chore: issue and epic store updated and code refactor

* chore: layout ux copy updated
This commit is contained in:
Anmol Singh Bhatia
2024-12-26 20:01:32 +05:30
committed by GitHub
parent 36b3328c5e
commit 756a71ca78
18 changed files with 71 additions and 50 deletions

View File

@@ -30,10 +30,11 @@ export type TQuickAddIssueFormRoot = {
register: UseFormRegister<TIssue>;
onSubmit: () => void;
onClose: () => void;
isEpic: boolean;
};
export const QuickAddIssueFormRoot: FC<TQuickAddIssueFormRoot> = observer((props) => {
const { isOpen, layout, projectId, hasError = false, setFocus, register, onSubmit, onClose } = props;
const { isOpen, layout, projectId, hasError = false, setFocus, register, onSubmit, onClose, isEpic } = props;
// store hooks
const { getProjectById } = useProject();
// derived values
@@ -70,6 +71,7 @@ export const QuickAddIssueFormRoot: FC<TQuickAddIssueFormRoot> = observer((props
hasError={hasError}
register={register}
onSubmit={onSubmit}
isEpic={isEpic}
/>
);
});

View File

@@ -93,10 +93,11 @@ export class IssueActivityStore implements IIssueActivityStore {
let activityComments: TIssueActivityComment[] = [];
const currentStore = this.serviceType === EIssueServiceType.EPICS ? this.store.epic : this.store.issue;
const currentStore =
this.serviceType === EIssueServiceType.EPICS ? this.store.issue.epicDetail : this.store.issue.issueDetail;
const activities = this.getActivitiesByIssueId(issueId) || [];
const comments = currentStore.issueDetail.comment.getCommentsByIssueId(issueId) || [];
const comments = currentStore.comment.getCommentsByIssueId(issueId) || [];
activities.forEach((activityId) => {
const activity = this.getActivityById(activityId);
@@ -109,7 +110,7 @@ export class IssueActivityStore implements IIssueActivityStore {
});
comments.forEach((commentId) => {
const comment = currentStore.issueDetail.comment.getCommentById(commentId);
const comment = currentStore.comment.getCommentById(commentId);
if (!comment) return;
activityComments.push({
id: comment.id,

View File

@@ -10,10 +10,11 @@ type TCreateIssueToastActionItems = {
workspaceSlug: string;
projectId: string;
issueId: string;
isEpic?: boolean;
};
export const CreateIssueToastActionItems: FC<TCreateIssueToastActionItems> = observer((props) => {
const { workspaceSlug, projectId, issueId } = props;
const { workspaceSlug, projectId, issueId, isEpic = false } = props;
// state
const [copied, setCopied] = useState(false);
// store hooks
@@ -26,7 +27,7 @@ export const CreateIssueToastActionItems: FC<TCreateIssueToastActionItems> = obs
if (!issue) return null;
const issueLink = `${workspaceSlug}/projects/${projectId}/issues/${issueId}`;
const issueLink = `${workspaceSlug}/projects/${projectId}/${isEpic ? "epics" : "issues"}/${issueId}`;
const copyToClipboard = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
try {
@@ -43,12 +44,12 @@ export const CreateIssueToastActionItems: FC<TCreateIssueToastActionItems> = obs
return (
<div className="flex items-center gap-1 text-xs text-custom-text-200">
<a
href={`/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`}
href={`/${workspaceSlug}/projects/${projectId}/${isEpic ? "epics" : "issues"}/${issueId}/`}
target="_blank"
rel="noopener noreferrer"
className="text-custom-primary px-2 py-1 hover:bg-custom-background-90 font-medium rounded"
>
View issue
{`View ${isEpic ? "epic" : "issue"}`}
</a>
{copied ? (

View File

@@ -24,10 +24,11 @@ type TCalendarQuickAddIssueActions = {
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
addIssuesToView?: (issueIds: string[]) => Promise<any>;
onOpen?: () => void;
isEpic?: boolean;
};
export const CalendarQuickAddIssueActions: FC<TCalendarQuickAddIssueActions> = observer((props) => {
const { prePopulatedData, quickAddCallback, addIssuesToView, onOpen } = props;
const { prePopulatedData, quickAddCallback, addIssuesToView, onOpen, isEpic = false } = props;
// router
const { workspaceSlug, projectId, moduleId } = useParams();
// states
@@ -118,15 +119,16 @@ export const CalendarQuickAddIssueActions: FC<TCalendarQuickAddIssueActions> = o
customButton={
<div className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-text-350 hover:text-custom-text-300">
<PlusIcon className="h-3.5 w-3.5 stroke-2 flex-shrink-0" />
<span className="text-sm font-medium flex-shrink-0">New issue</span>
<span className="text-sm font-medium flex-shrink-0">{`New ${isEpic ? "Epic" : "Issue"}`}</span>
</div>
}
>
<CustomMenu.MenuItem onClick={handleNewIssue}>New issue</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleExistingIssue}>Add existing issue</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleNewIssue}>{`New ${isEpic ? "Epic" : "Issue"}`}</CustomMenu.MenuItem>
{!isEpic && <CustomMenu.MenuItem onClick={handleExistingIssue}>Add existing issue</CustomMenu.MenuItem>}
</CustomMenu>
</div>
}
isEpic={isEpic}
/>
</>
);

View File

@@ -98,6 +98,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
groupBy: group_by as GroupByColumnTypes,
includeNone: true,
isWorkspaceLevel: isWorkspaceLevel(storeType),
isEpic: isEpic,
});
if (!list) return null;

View File

@@ -82,7 +82,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
quickAddCallback,
scrollableContainerRef,
handleOnDrop,
isEpic =false
isEpic = false,
} = props;
// hooks
const projectState = useProjectState();
@@ -285,6 +285,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
dropErrorMessage={dropErrorMessage}
orderBy={orderBy}
isDraggingOverColumn={isDraggingOverColumn}
isEpic={isEpic}
/>
<KanbanIssueBlocksList
sub_group_id={sub_group_id}
@@ -312,6 +313,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
...(group_by && prePopulateQuickAddData(group_by, sub_group_by, groupId, sub_group_id)),
}}
quickAddCallback={quickAddCallback}
isEpic={isEpic}
/>
</div>
)}

View File

@@ -84,6 +84,7 @@ export const List: React.FC<IList> = observer((props) => {
groupBy: group_by as GroupByColumnTypes,
includeNone: true,
isWorkspaceLevel: isWorkspaceLevel(storeType),
isEpic: isEpic,
});
// Enable Auto Scroll for Main Kanban

View File

@@ -3,12 +3,13 @@ import { observer } from "mobx-react";
import { TQuickAddIssueForm } from "../root";
export const CalendarQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props) => {
const { ref, isOpen, projectDetail, register, onSubmit } = props;
const { ref, isOpen, projectDetail, register, onSubmit, isEpic } = props;
return (
<div
className={`z-20 w-full transition-all ${isOpen ? "scale-100 opacity-100" : "pointer-events-none scale-95 opacity-0"
}`}
className={`z-20 w-full transition-all ${
isOpen ? "scale-100 opacity-100" : "pointer-events-none scale-95 opacity-0"
}`}
>
<form
ref={ref}
@@ -19,9 +20,9 @@ export const CalendarQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
placeholder={isEpic ? "Epic Title" : "Issue Title"}
{...register("name", {
required: "Issue title is required.",
required: `${isEpic ? "Epic" : "Issue"} title is required.`,
})}
className="w-full rounded-md bg-transparent py-1.5 pr-2 text-sm md:text-xs font-medium leading-5 text-custom-text-200 outline-none"
/>

View File

@@ -4,7 +4,7 @@ import { cn } from "@/helpers/common.helper";
import { TQuickAddIssueForm } from "../root";
export const GanttQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props) => {
const { ref, projectDetail, hasError, register, onSubmit } = props;
const { ref, projectDetail, hasError, register, onSubmit, isEpic } = props;
return (
<div className={cn("shadow-custom-shadow-sm", hasError && "border border-red-500/20 bg-red-500/10")}>
@@ -18,15 +18,15 @@ export const GanttQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props) =
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
placeholder={isEpic ? "Epic Title" : "Issue Title"}
{...register("name", {
required: "Issue title is required.",
required: `${isEpic ? "Epic" : "Issue"} title is required.`,
})}
className="w-full rounded-md bg-transparent px-2 py-3 text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</div>
</form>
<div className="px-3 py-2 text-xs bg-custom-background-100 italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
<div className="px-3 py-2 text-xs bg-custom-background-100 italic text-custom-text-200">{`Press 'Enter' to add another ${isEpic ? "epic" : "issue"}`}</div>
</div>
);
});

View File

@@ -3,7 +3,7 @@ import { observer } from "mobx-react";
import { TQuickAddIssueForm } from "../root";
export const KanbanQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props) => {
const { ref, projectDetail, register, onSubmit } = props;
const { ref, projectDetail, register, onSubmit, isEpic } = props;
return (
<div className="m-1 overflow-hidden rounded shadow-custom-shadow-sm">
@@ -12,7 +12,7 @@ export const KanbanQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props)
<h4 className="text-xs font-medium leading-5 text-custom-text-300">{projectDetail?.identifier ?? "..."}</h4>
<input
autoComplete="off"
placeholder="Issue Title"
placeholder={isEpic ? "Epic Title" : "Issue Title"}
{...register("name", {
required: "Issue title is required.",
})}
@@ -20,7 +20,7 @@ export const KanbanQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props)
/>
</div>
</form>
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another ${isEpic ? "epic" : "issue"}`}</div>
</div>
);
});

View File

@@ -3,7 +3,7 @@ import { observer } from "mobx-react";
import { TQuickAddIssueForm } from "../root";
export const ListQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props) => {
const { ref, projectDetail, register, onSubmit } = props;
const { ref, projectDetail, register, onSubmit, isEpic } = props;
return (
<div className="shadow-custom-shadow-sm">
@@ -17,15 +17,15 @@ export const ListQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props) =>
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
placeholder={isEpic ? "Epic Title" : "Issue Title"}
{...register("name", {
required: "Issue title is required.",
required: `${isEpic ? "Epic" : "Issue"} title is required.`,
})}
className="w-full rounded-md bg-transparent px-2 py-3 text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</div>
</form>
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another ${isEpic ? "epic" : "issue"}`}</div>
</div>
);
});

View File

@@ -3,7 +3,7 @@ import { observer } from "mobx-react";
import { TQuickAddIssueForm } from "../root";
export const SpreadsheetQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((props) => {
const { ref, projectDetail, register, onSubmit } = props;
const { ref, projectDetail, register, onSubmit, isEpic } = props;
return (
<div className="pb-2">
@@ -16,15 +16,15 @@ export const SpreadsheetQuickAddIssueForm: FC<TQuickAddIssueForm> = observer((pr
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
placeholder={isEpic ? "Epic Title" : "Issue Title"}
{...register("name", {
required: "Issue title is required.",
required: `${isEpic ? "Epic" : "Issue"} title is required.`,
})}
className="w-full rounded-md bg-transparent py-3 text-sm leading-5 text-custom-text-200 outline-none"
/>
</form>
<p className="ml-3 mt-3 text-xs italic text-custom-text-200">
Press {"'"}Enter{"'"} to add another issue
{`Press Enter to add another ${isEpic ? "epic" : "issue"}`}
</p>
</div>
);

View File

@@ -6,7 +6,7 @@ import { useParams, usePathname } from "next/navigation";
import { useForm, UseFormRegister } from "react-hook-form";
import { PlusIcon } from "lucide-react";
// plane constants
import { EIssueLayoutTypes } from "@plane/constants";
import { EIssueLayoutTypes, EIssueServiceType } from "@plane/constants";
// types
import { IProject, TIssue } from "@plane/types";
// ui
@@ -30,9 +30,11 @@ export type TQuickAddIssueForm = {
hasError: boolean;
register: UseFormRegister<TIssue>;
onSubmit: () => void;
isEpic: boolean;
};
export type TQuickAddIssueButton = {
isEpic?: boolean;
onClick: () => void;
};
@@ -45,6 +47,7 @@ type TQuickAddIssueRoot = {
containerClassName?: string;
setIsQuickAddOpen?: (isOpen: boolean) => void;
quickAddCallback?: (projectId: string | null | undefined, data: TIssue) => Promise<TIssue | undefined>;
isEpic?: boolean;
};
const defaultValues: Partial<TIssue> = {
@@ -61,6 +64,7 @@ export const QuickAddIssueRoot: FC<TQuickAddIssueRoot> = observer((props) => {
containerClassName = "",
setIsQuickAddOpen,
quickAddCallback,
isEpic = false,
} = props;
// router
const { workspaceSlug, projectId } = useParams();
@@ -109,15 +113,16 @@ export const QuickAddIssueRoot: FC<TQuickAddIssueRoot> = observer((props) => {
if (quickAddCallback) {
const quickAddPromise = quickAddCallback(projectId.toString(), { ...payload });
setPromiseToast<any>(quickAddPromise, {
loading: "Adding issue...",
loading: `Adding ${isEpic ? "epic" : "issue"}...`,
success: {
title: "Success!",
message: () => "Issue created successfully.",
message: () => `${isEpic ? "Epic" : "Issue"} created successfully.`,
actionItems: (data) => (
<CreateIssueToastActionItems
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
issueId={data.id}
isEpic={isEpic}
/>
),
},
@@ -165,10 +170,11 @@ export const QuickAddIssueRoot: FC<TQuickAddIssueRoot> = observer((props) => {
register={register}
onSubmit={handleSubmit(onSubmitHandler)}
onClose={() => handleIsOpen(false)}
isEpic={isEpic}
/>
) : (
<>
{QuickAddButton && <QuickAddButton onClick={() => handleIsOpen(true)} />}
{QuickAddButton && <QuickAddButton isEpic={isEpic} onClick={() => handleIsOpen(true)} />}
{customQuickAddButton && <>{customQuickAddButton}</>}
{!QuickAddButton && !customQuickAddButton && (
<div
@@ -176,7 +182,7 @@ export const QuickAddIssueRoot: FC<TQuickAddIssueRoot> = observer((props) => {
onClick={() => handleIsOpen(true)}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium">New Issue</span>
<span className="text-sm font-medium">{`New ${isEpic ? "Epic" : "Issue"}`}</span>
</div>
)}
</>

View File

@@ -68,6 +68,7 @@ type TGetGroupByColumns = {
groupBy: GroupByColumnTypes | null;
includeNone: boolean;
isWorkspaceLevel: boolean;
isEpic?: boolean;
};
// NOTE: Type of groupBy is different compared to what's being passed from the components.
@@ -77,13 +78,14 @@ export const getGroupByColumns = ({
groupBy,
includeNone,
isWorkspaceLevel,
isEpic = false,
}: TGetGroupByColumns): IGroupByColumn[] | undefined => {
// If no groupBy is specified and includeNone is true, return "All Issues" group
if (!groupBy && includeNone) {
return [
{
id: "All Issues",
name: "All Issues",
name: isEpic ? "All Epics" : "All Issues",
payload: {},
icon: undefined,
},

View File

@@ -9,6 +9,6 @@ import { IIssueDetail } from "@/store/issue/issue-details/root.store";
export const useIssueDetail = (serviceType: TIssueServiceType = EIssueServiceType.ISSUES): IIssueDetail => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useIssueDetail must be used within StoreProvider");
if (serviceType === EIssueServiceType.EPICS) return context.epic.issueDetail;
if (serviceType === EIssueServiceType.EPICS) return context.issue.epicDetail;
else return context.issue.issueDetail;
};

View File

@@ -3,7 +3,7 @@ import update from "lodash/update";
import { action, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
// types
import { TIssue, TIssueServiceType } from "@plane/types";
import { TIssue } from "@plane/types";
// helpers
import { getCurrentDateTimeInISO } from "@/helpers/date-time.helper";
// services
@@ -30,7 +30,7 @@ export class IssueStore implements IIssueStore {
// service
issueService;
constructor(serviceType: TIssueServiceType) {
constructor() {
makeObservable(this, {
// observable
issuesMap: observable,
@@ -39,8 +39,7 @@ export class IssueStore implements IIssueStore {
updateIssue: action,
removeIssue: action,
});
this.issueService = new IssueService(serviceType);
this.issueService = new IssueService();
}
// actions
@@ -85,7 +84,10 @@ export class IssueStore implements IIssueStore {
set(this.issuesMap, [issueId, key], issue[key as keyof TIssue]);
});
});
updatePersistentLayer(issueId);
if (!this.issuesMap[issueId]?.is_epic) {
updatePersistentLayer(issueId);
}
};
/**

View File

@@ -67,6 +67,7 @@ export interface IIssueRootStore {
issues: IIssueStore;
issueDetail: IIssueDetail;
epicDetail: IIssueDetail;
workspaceIssuesFilter: IWorkspaceIssuesFilter;
workspaceIssues: IWorkspaceIssues;
@@ -134,6 +135,7 @@ export class IssueRootStore implements IIssueRootStore {
issues: IIssueStore;
issueDetail: IIssueDetail;
epicDetail: IIssueDetail;
workspaceIssuesFilter: IWorkspaceIssuesFilter;
workspaceIssues: IWorkspaceIssues;
@@ -221,9 +223,10 @@ export class IssueRootStore implements IIssueRootStore {
if (!isEmpty(rootStore?.cycle?.cycleMap)) this.cycleMap = rootStore?.cycle?.cycleMap;
});
this.issues = new IssueStore(this.serviceType);
this.issues = new IssueStore();
this.issueDetail = new IssueDetail(this, this.serviceType);
this.issueDetail = new IssueDetail(this, EIssueServiceType.ISSUES);
this.epicDetail = new IssueDetail(this, EIssueServiceType.EPICS);
this.workspaceIssuesFilter = new WorkspaceIssuesFilter(this);
this.workspaceIssues = new WorkspaceIssues(this, this.workspaceIssuesFilter);

View File

@@ -43,7 +43,6 @@ export class CoreRootStore {
projectView: IProjectViewStore;
globalView: IGlobalViewStore;
issue: IIssueRootStore;
epic: IIssueRootStore;
state: IStateStore;
label: ILabelStore;
dashboard: IDashboardStore;
@@ -77,7 +76,6 @@ export class CoreRootStore {
this.projectView = new ProjectViewStore(this);
this.globalView = new GlobalViewStore(this);
this.issue = new IssueRootStore(this as unknown as RootStore);
this.epic = new IssueRootStore(this as unknown as RootStore, EIssueServiceType.EPICS);
this.state = new StateStore(this as unknown as RootStore);
this.label = new LabelStore(this);
this.dashboard = new DashboardStore(this);
@@ -109,7 +107,6 @@ export class CoreRootStore {
this.projectView = new ProjectViewStore(this);
this.globalView = new GlobalViewStore(this);
this.issue = new IssueRootStore(this as unknown as RootStore);
this.epic = new IssueRootStore(this as unknown as RootStore, EIssueServiceType.EPICS);
this.state = new StateStore(this as unknown as RootStore);
this.label = new LabelStore(this);
this.dashboard = new DashboardStore(this);