chore: sub issue mutation for state and project change (#3442)

* chore: sub-issues mutation

* chore: posthog un commented
This commit is contained in:
guru_sainath 2024-01-23 16:56:22 +05:30 committed by GitHub
parent d9db765ae3
commit c1e1b81b99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 146 additions and 46 deletions

View File

@ -32,11 +32,11 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
workspaceSlug, workspaceSlug,
projectId, projectId,
parentIssueId, parentIssueId,
issueId,
spacingLeft = 10, spacingLeft = 10,
disabled, disabled,
handleIssueCrudState, handleIssueCrudState,
subIssueOperations, subIssueOperations,
issueId,
} = props; } = props;
const { const {
@ -81,12 +81,12 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
<div <div
className="flex h-full w-full cursor-pointer items-center justify-center rounded-sm transition-all hover:bg-custom-background-80" className="flex h-full w-full cursor-pointer items-center justify-center rounded-sm transition-all hover:bg-custom-background-80"
onClick={async () => { onClick={async () => {
if (!subIssueHelpers.issue_visibility.includes(issue.id)) { if (!subIssueHelpers.issue_visibility.includes(issueId)) {
setSubIssueHelpers(parentIssueId, "preview_loader", issue.id); setSubIssueHelpers(parentIssueId, "preview_loader", issueId);
await subIssueOperations.fetchSubIssues(workspaceSlug, projectId, issue.id); await subIssueOperations.fetchSubIssues(workspaceSlug, projectId, issueId);
setSubIssueHelpers(parentIssueId, "preview_loader", issue.id); setSubIssueHelpers(parentIssueId, "preview_loader", issueId);
} }
setSubIssueHelpers(parentIssueId, "issue_visibility", issue.id); setSubIssueHelpers(parentIssueId, "issue_visibility", issueId);
}} }}
> >
{subIssueHelpers.issue_visibility.includes(issue.id) ? ( {subIssueHelpers.issue_visibility.includes(issue.id) ? (
@ -136,7 +136,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
<div className="flex-shrink-0 text-sm"> <div className="flex-shrink-0 text-sm">
<CustomMenu placement="bottom-end" ellipsis> <CustomMenu placement="bottom-end" ellipsis>
{disabled && ( {disabled && (
<CustomMenu.MenuItem onClick={() => handleIssueCrudState("update", parentIssueId, issue)}> <CustomMenu.MenuItem onClick={() => handleIssueCrudState("update", parentIssueId, { ...issue })}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Pencil className="h-3.5 w-3.5" strokeWidth={2} /> <Pencil className="h-3.5 w-3.5" strokeWidth={2} />
<span>Edit issue</span> <span>Edit issue</span>

View File

@ -34,16 +34,13 @@ export const IssueList: FC<IIssueList> = observer((props) => {
} = props; } = props;
// hooks // hooks
const { const {
subIssues: { subIssuesByIssueId, subIssueHelpersByIssueId }, subIssues: { subIssuesByIssueId },
} = useIssueDetail(); } = useIssueDetail();
const subIssueIds = subIssuesByIssueId(parentIssueId); const subIssueIds = subIssuesByIssueId(parentIssueId);
const subIssueHelpers = subIssueHelpersByIssueId(parentIssueId);
return ( return (
<> <>
{subIssueHelpers.preview_loader.includes(parentIssueId) ? "Loading..." : null}
<div className="relative"> <div className="relative">
{subIssueIds && {subIssueIds &&
subIssueIds.length > 0 && subIssueIds.length > 0 &&
@ -53,11 +50,11 @@ export const IssueList: FC<IIssueList> = observer((props) => {
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
parentIssueId={parentIssueId} parentIssueId={parentIssueId}
issueId={issueId}
spacingLeft={spacingLeft} spacingLeft={spacingLeft}
disabled={disabled} disabled={disabled}
handleIssueCrudState={handleIssueCrudState} handleIssueCrudState={handleIssueCrudState}
subIssueOperations={subIssueOperations} subIssueOperations={subIssueOperations}
issueId={issueId}
/> />
</> </>
))} ))}

View File

@ -31,9 +31,16 @@ export const IssueProperty: React.FC<IIssueProperty> = (props) => {
value={issue.state_id} value={issue.state_id}
projectId={issue.project_id} projectId={issue.project_id}
onChange={(val) => onChange={(val) =>
subIssueOperations.updateSubIssue(workspaceSlug, issue.project_id, parentIssueId, issueId, { subIssueOperations.updateSubIssue(
state_id: val, workspaceSlug,
}) issue.project_id,
parentIssueId,
issueId,
{
state_id: val,
},
{ ...issue }
)
} }
disabled={!disabled} disabled={!disabled}
buttonVariant="border-with-text" buttonVariant="border-with-text"

View File

@ -1,7 +1,6 @@
import { FC, useMemo, useState } from "react"; import { FC, useMemo, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Plus, ChevronRight, ChevronDown, Loader } from "lucide-react"; import { Plus, ChevronRight, ChevronDown, Loader } from "lucide-react";
import useSWR from "swr";
// hooks // hooks
import { useIssueDetail } from "hooks/store"; import { useIssueDetail } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
@ -34,7 +33,9 @@ export type TSubIssueOperations = {
projectId: string, projectId: string,
parentIssueId: string, parentIssueId: string,
issueId: string, issueId: string,
data: Partial<TIssue> issueData: Partial<TIssue>,
oldIssue?: Partial<TIssue>,
fromModal?: boolean
) => Promise<void>; ) => Promise<void>;
removeSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>; removeSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
deleteSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>; deleteSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
@ -84,18 +85,6 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
}, },
}); });
useSWR(
workspaceSlug && projectId && parentIssueId
? `ISSUE_DETAIL_SUB_ISSUES_${workspaceSlug}_${projectId}_${parentIssueId}`
: null,
async () => {
workspaceSlug &&
projectId &&
parentIssueId &&
(await subIssueOperations.fetchSubIssues(workspaceSlug, projectId, parentIssueId));
}
);
const handleIssueCrudState = ( const handleIssueCrudState = (
key: "create" | "existing" | "update" | "delete", key: "create" | "existing" | "update" | "delete",
_parentIssueId: string | null, _parentIssueId: string | null,
@ -155,11 +144,13 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
projectId: string, projectId: string,
parentIssueId: string, parentIssueId: string,
issueId: string, issueId: string,
data: Partial<TIssue> issueData: Partial<TIssue>,
oldIssue: Partial<TIssue> = {},
fromModal: boolean = false
) => { ) => {
try { try {
setSubIssueHelpers(parentIssueId, "issue_loader", issueId); setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
await updateSubIssue(workspaceSlug, projectId, parentIssueId, issueId, data); await updateSubIssue(workspaceSlug, projectId, parentIssueId, issueId, issueData, oldIssue, fromModal);
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Sub-issue updated successfully", title: "Sub-issue updated successfully",
@ -231,7 +222,14 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
<div className="relative flex items-center gap-4 text-xs"> <div className="relative flex items-center gap-4 text-xs">
<div <div
className="flex cursor-pointer select-none items-center gap-1 rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80" className="flex cursor-pointer select-none items-center gap-1 rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80"
onClick={() => setSubIssueHelpers(`${parentIssueId}_root`, "issue_visibility", parentIssueId)} onClick={async () => {
if (!subIssueHelpers.issue_visibility.includes(parentIssueId)) {
setSubIssueHelpers(`${parentIssueId}_root`, "preview_loader", parentIssueId);
await subIssueOperations.fetchSubIssues(workspaceSlug, projectId, parentIssueId);
setSubIssueHelpers(`${parentIssueId}_root`, "preview_loader", parentIssueId);
}
setSubIssueHelpers(`${parentIssueId}_root`, "issue_visibility", parentIssueId);
}}
> >
<div className="flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center"> <div className="flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center">
{subIssueHelpers.preview_loader.includes(parentIssueId) ? ( {subIssueHelpers.preview_loader.includes(parentIssueId) ? (
@ -395,7 +393,15 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
}} }}
data={issueCrudState?.update?.issue ?? undefined} data={issueCrudState?.update?.issue ?? undefined}
onSubmit={async (_issue: TIssue) => { onSubmit={async (_issue: TIssue) => {
await subIssueOperations.updateSubIssue(workspaceSlug, projectId, parentIssueId, _issue.id, _issue); await subIssueOperations.updateSubIssue(
workspaceSlug,
projectId,
parentIssueId,
_issue.id,
_issue,
issueCrudState?.update?.issue,
true
);
}} }}
/> />
</> </>

View File

@ -89,6 +89,9 @@ export class IssueStore implements IIssueStore {
// fetch issue relations // fetch issue relations
this.rootIssueDetailStore.relation.fetchRelations(workspaceSlug, projectId, issueId); this.rootIssueDetailStore.relation.fetchRelations(workspaceSlug, projectId, issueId);
// fetching states
this.rootIssueDetailStore.rootIssueStore.state.fetchProjectStates(workspaceSlug, projectId);
return issue; return issue;
} catch (error) { } catch (error) {
throw error; throw error;

View File

@ -183,8 +183,10 @@ export class IssueDetail implements IIssueDetail {
projectId: string, projectId: string,
parentIssueId: string, parentIssueId: string,
issueId: string, issueId: string,
data: Partial<TIssue> issueData: Partial<TIssue>,
) => this.subIssues.updateSubIssue(workspaceSlug, projectId, parentIssueId, issueId, data); oldIssue?: Partial<TIssue>,
fromModal?: boolean
) => this.subIssues.updateSubIssue(workspaceSlug, projectId, parentIssueId, issueId, issueData, oldIssue, fromModal);
removeSubIssue = async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => removeSubIssue = async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) =>
this.subIssues.removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId); this.subIssues.removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
deleteSubIssue = async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => deleteSubIssue = async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) =>

View File

@ -28,7 +28,9 @@ export interface IIssueSubIssuesStoreActions {
projectId: string, projectId: string,
parentIssueId: string, parentIssueId: string,
issueId: string, issueId: string,
data: Partial<TIssue> issueData: Partial<TIssue>,
oldIssue?: Partial<TIssue>,
fromModal?: boolean
) => Promise<void>; ) => Promise<void>;
removeSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>; removeSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
deleteSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>; deleteSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
@ -100,11 +102,9 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
setSubIssueHelpers = (parentIssueId: string, key: TSubIssueHelpersKeys, value: string) => { setSubIssueHelpers = (parentIssueId: string, key: TSubIssueHelpersKeys, value: string) => {
if (!parentIssueId || !key || !value) return; if (!parentIssueId || !key || !value) return;
update(this.subIssueHelpers, [parentIssueId, key], (subIssueHelpers: string[]) => { update(this.subIssueHelpers, [parentIssueId, key], (_subIssueHelpers: string[] = []) => {
if (!subIssueHelpers || subIssueHelpers.length <= 0) return [value]; if (_subIssueHelpers.includes(value)) return pull(_subIssueHelpers, value);
else if (subIssueHelpers.includes(value)) pull(subIssueHelpers, value); return concat(_subIssueHelpers, value);
else concat(subIssueHelpers, value);
return subIssueHelpers;
}); });
}; };
@ -169,16 +169,60 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
projectId: string, projectId: string,
parentIssueId: string, parentIssueId: string,
issueId: string, issueId: string,
data: Partial<TIssue> issueData: Partial<TIssue>,
oldIssue: Partial<TIssue> = {},
fromModal: boolean = false
) => { ) => {
try { try {
await this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); if (!fromModal)
await this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue(
workspaceSlug,
projectId,
issueId,
issueData
);
if (data.hasOwnProperty("parent_id") && data.parent_id !== parentIssueId) { // parent update
if (issueData.hasOwnProperty("parent_id") && issueData.parent_id !== oldIssue.parent_id) {
runInAction(() => { runInAction(() => {
pull(this.subIssues[parentIssueId], issueId); if (oldIssue.parent_id) pull(this.subIssues[oldIssue.parent_id], issueId);
if (issueData.parent_id)
set(this.subIssues, [issueData.parent_id], concat(this.subIssues[issueData.parent_id], issueId));
}); });
} }
// state update
if (issueData.hasOwnProperty("state_id") && issueData.state_id !== oldIssue.state_id) {
let oldIssueStateGroup: string | undefined = undefined;
let issueStateGroup: string | undefined = undefined;
if (oldIssue.state_id) {
const state = this.rootIssueDetailStore.rootIssueStore.state.getStateById(oldIssue.state_id);
if (state?.group) oldIssueStateGroup = state.group;
}
if (issueData.state_id) {
const state = this.rootIssueDetailStore.rootIssueStore.state.getStateById(issueData.state_id);
if (state?.group) issueStateGroup = state.group;
}
if (oldIssueStateGroup && issueStateGroup && issueStateGroup !== oldIssueStateGroup) {
runInAction(() => {
if (oldIssueStateGroup)
update(this.subIssuesStateDistribution, [parentIssueId, oldIssueStateGroup], (stateDistribution) => {
if (!stateDistribution) return;
return pull(stateDistribution, issueId);
});
if (issueStateGroup)
update(this.subIssuesStateDistribution, [parentIssueId, issueStateGroup], (stateDistribution) => {
if (!stateDistribution) return [issueId];
return concat(stateDistribution, issueId);
});
});
}
}
return; return;
} catch (error) { } catch (error) {
throw error; throw error;
@ -191,6 +235,23 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
parent_id: null, parent_id: null,
}); });
const issue = this.rootIssueDetailStore.issue.getIssueById(issueId);
if (issue && issue.state_id) {
let issueStateGroup: string | undefined = undefined;
const state = this.rootIssueDetailStore.rootIssueStore.state.getStateById(issue.state_id);
if (state?.group) issueStateGroup = state.group;
if (issueStateGroup) {
runInAction(() => {
if (issueStateGroup)
update(this.subIssuesStateDistribution, [parentIssueId, issueStateGroup], (stateDistribution) => {
if (!stateDistribution) return;
return pull(stateDistribution, issueId);
});
});
}
}
runInAction(() => { runInAction(() => {
pull(this.subIssues[parentIssueId], issueId); pull(this.subIssues[parentIssueId], issueId);
}); });
@ -205,6 +266,23 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
try { try {
await this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); await this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
const issue = this.rootIssueDetailStore.issue.getIssueById(issueId);
if (issue && issue.state_id) {
let issueStateGroup: string | undefined = undefined;
const state = this.rootIssueDetailStore.rootIssueStore.state.getStateById(issue.state_id);
if (state?.group) issueStateGroup = state.group;
if (issueStateGroup) {
runInAction(() => {
if (issueStateGroup)
update(this.subIssuesStateDistribution, [parentIssueId, issueStateGroup], (stateDistribution) => {
if (!stateDistribution) return;
return pull(stateDistribution, issueId);
});
});
}
}
runInAction(() => { runInAction(() => {
pull(this.subIssues[parentIssueId], issueId); pull(this.subIssues[parentIssueId], issueId);
}); });

View File

@ -2,6 +2,7 @@ import { autorun, makeObservable, observable } from "mobx";
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
// root store // root store
import { RootStore } from "../root.store"; import { RootStore } from "../root.store";
import { IStateStore, StateStore } from "../state.store";
// issues data store // issues data store
import { IState } from "@plane/types"; import { IState } from "@plane/types";
import { IIssueStore, IssueStore } from "./issue.store"; import { IIssueStore, IssueStore } from "./issue.store";
@ -39,6 +40,8 @@ export interface IIssueRootStore {
issues: IIssueStore; issues: IIssueStore;
state: IStateStore;
issueDetail: IIssueDetail; issueDetail: IIssueDetail;
workspaceIssuesFilter: IWorkspaceIssuesFilter; workspaceIssuesFilter: IWorkspaceIssuesFilter;
@ -86,6 +89,8 @@ export class IssueRootStore implements IIssueRootStore {
issues: IIssueStore; issues: IIssueStore;
state: IStateStore;
issueDetail: IIssueDetail; issueDetail: IIssueDetail;
workspaceIssuesFilter: IWorkspaceIssuesFilter; workspaceIssuesFilter: IWorkspaceIssuesFilter;
@ -151,6 +156,8 @@ export class IssueRootStore implements IIssueRootStore {
this.issues = new IssueStore(); this.issues = new IssueStore();
this.state = new StateStore(rootStore);
this.issueDetail = new IssueDetail(this); this.issueDetail = new IssueDetail(this);
this.workspaceIssuesFilter = new WorkspaceIssuesFilter(this); this.workspaceIssuesFilter = new WorkspaceIssuesFilter(this);

View File

@ -1,5 +1,5 @@
import { makeObservable, observable, computed, action, runInAction } from "mobx"; import { makeObservable, observable, computed, action, runInAction } from "mobx";
import { computedFn } from "mobx-utils" import { computedFn } from "mobx-utils";
import groupBy from "lodash/groupBy"; import groupBy from "lodash/groupBy";
import set from "lodash/set"; import set from "lodash/set";
// store // store