forked from github/plane
chore: issue update status in peek view
This commit is contained in:
parent
fa990ed444
commit
9d7b0c6daa
@ -25,19 +25,32 @@ interface IPeekOverviewIssueDetails {
|
|||||||
issueUpdate: (issue: Partial<IIssue>) => void;
|
issueUpdate: (issue: Partial<IIssue>) => void;
|
||||||
issueReactionCreate: (reaction: string) => void;
|
issueReactionCreate: (reaction: string) => void;
|
||||||
issueReactionRemove: (reaction: string) => void;
|
issueReactionRemove: (reaction: string) => void;
|
||||||
|
setShowAlert: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) => {
|
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) => {
|
||||||
const { workspaceSlug, issue, issueReactions, user, issueUpdate, issueReactionCreate, issueReactionRemove } = props;
|
const {
|
||||||
|
workspaceSlug,
|
||||||
|
issue,
|
||||||
|
issueReactions,
|
||||||
|
user,
|
||||||
|
issueUpdate,
|
||||||
|
issueReactionCreate,
|
||||||
|
issueReactionRemove,
|
||||||
|
setShowAlert,
|
||||||
|
} = props;
|
||||||
// store
|
// store
|
||||||
const { user: userStore } = useMobxStore();
|
const {
|
||||||
|
user: userStore,
|
||||||
|
projectIssues: { isSubmitting, setIsSubmitting },
|
||||||
|
} = useMobxStore();
|
||||||
const { currentProjectRole } = userStore;
|
const { currentProjectRole } = userStore;
|
||||||
const isAllowed = [15, 20].includes(currentProjectRole || 0);
|
const isAllowed = [15, 20].includes(currentProjectRole || 0);
|
||||||
// states
|
// states
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
|
||||||
const [characterLimit, setCharacterLimit] = useState(false);
|
const [characterLimit, setCharacterLimit] = useState(false);
|
||||||
// hooks
|
// hooks
|
||||||
const { setShowAlert } = useReloadConfirmations();
|
|
||||||
const editorSuggestions = useEditorSuggestions();
|
const editorSuggestions = useEditorSuggestions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -75,20 +88,9 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
}, [issueTitleCurrentValue, localTitleValue]);
|
}, [issueTitleCurrentValue, localTitleValue]);
|
||||||
|
|
||||||
const debouncedFormSave = debounce(async () => {
|
const debouncedFormSave = debounce(async () => {
|
||||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
handleSubmit(handleDescriptionFormSubmit)();
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isSubmitting === "submitted") {
|
|
||||||
setShowAlert(false);
|
|
||||||
setTimeout(async () => {
|
|
||||||
setIsSubmitting("saved");
|
|
||||||
}, 2000);
|
|
||||||
} else if (isSubmitting === "submitting") {
|
|
||||||
setShowAlert(true);
|
|
||||||
}
|
|
||||||
}, [isSubmitting, setShowAlert]);
|
|
||||||
|
|
||||||
// reset form values
|
// reset form values
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!issue) return;
|
if (!issue) return;
|
||||||
@ -171,13 +173,6 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
|
|
||||||
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<IssueReaction
|
<IssueReaction
|
||||||
issueReactions={issueReactions}
|
issueReactions={issueReactions}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { FC, ReactNode, useState } from "react";
|
import { FC, ReactNode, useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { MoveRight, MoveDiagonal, Bell, Link2, Trash2 } from "lucide-react";
|
import { MoveRight, MoveDiagonal, Bell, Link2, Trash2, RefreshCw } from "lucide-react";
|
||||||
// components
|
// components
|
||||||
import { PeekOverviewIssueDetails } from "./issue-detail";
|
import { PeekOverviewIssueDetails } from "./issue-detail";
|
||||||
import { PeekOverviewProperties } from "./properties";
|
import { PeekOverviewProperties } from "./properties";
|
||||||
@ -14,6 +14,7 @@ import { DeleteArchivedIssueModal } from "../delete-archived-issue-modal";
|
|||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||||
|
|
||||||
interface IIssueView {
|
interface IIssueView {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -89,11 +90,17 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { peekIssueId } = router.query as { peekIssueId: string };
|
const { peekIssueId } = router.query as { peekIssueId: string };
|
||||||
|
|
||||||
const { user: userStore, issueDetail: issueDetailStore } = useMobxStore();
|
const {
|
||||||
|
user: userStore,
|
||||||
|
issueDetail: issueDetailStore,
|
||||||
|
projectIssues: { isSubmitting, setIsSubmitting },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
||||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||||
|
|
||||||
|
const { setShowAlert } = useReloadConfirmations();
|
||||||
|
|
||||||
const updateRoutePeekId = () => {
|
const updateRoutePeekId = () => {
|
||||||
if (issueId != peekIssueId) {
|
if (issueId != peekIssueId) {
|
||||||
issueDetailStore.setPeekId(issueId);
|
issueDetailStore.setPeekId(issueId);
|
||||||
@ -136,6 +143,17 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
|
|
||||||
const currentMode = peekOptions.find((m) => m.key === peekMode);
|
const currentMode = peekOptions.find((m) => m.key === peekMode);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSubmitting === "submitted") {
|
||||||
|
setShowAlert(false);
|
||||||
|
setTimeout(async () => {
|
||||||
|
setIsSubmitting("saved");
|
||||||
|
}, 2000);
|
||||||
|
} else if (isSubmitting === "submitting") {
|
||||||
|
setShowAlert(true);
|
||||||
|
}
|
||||||
|
}, [isSubmitting, setShowAlert]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{issue && !isArchived && (
|
{issue && !isArchived && (
|
||||||
@ -235,12 +253,20 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
{issueSubscription && issueSubscription.subscribed ? "Unsubscribe" : "Subscribe"}
|
{issueSubscription && issueSubscription.subscribed ? "Unsubscribe" : "Subscribe"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<div className={`flex items-center gap-x-2 ${isSubmitting === "saved" ? "fadeOut" : "fadeIn"}`}>
|
||||||
|
{isSubmitting !== "submitted" && isSubmitting !== "saved" && (
|
||||||
|
<RefreshCw className="h-4 w-4 stroke-custom-text-300" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-custom-text-300">
|
||||||
|
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<button onClick={handleCopyText}>
|
<button onClick={handleCopyText}>
|
||||||
<Link2 className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200 -rotate-45" />
|
<Link2 className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200 -rotate-45" />
|
||||||
</button>
|
</button>
|
||||||
{!disableUserActions && (
|
{!disableUserActions && (
|
||||||
<button onClick={() => setDeleteIssueModal(true)}>
|
<button onClick={() => setDeleteIssueModal(true)}>
|
||||||
<Trash2 className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
<Trash2 className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -261,6 +287,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
<div className="absolute top-0 left-0 h-full min-h-full w-full z-[9] flex items-center justify-center bg-custom-background-100 opacity-60" />
|
<div className="absolute top-0 left-0 h-full min-h-full w-full z-[9] flex items-center justify-center bg-custom-background-100 opacity-60" />
|
||||||
)}
|
)}
|
||||||
<PeekOverviewIssueDetails
|
<PeekOverviewIssueDetails
|
||||||
|
setShowAlert={setShowAlert}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
issueUpdate={issueUpdate}
|
issueUpdate={issueUpdate}
|
||||||
@ -295,6 +322,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
<div className="relative w-full h-full space-y-6 p-4 py-5 overflow-auto">
|
<div className="relative w-full h-full space-y-6 p-4 py-5 overflow-auto">
|
||||||
<div className={isArchived ? "pointer-events-none" : ""}>
|
<div className={isArchived ? "pointer-events-none" : ""}>
|
||||||
<PeekOverviewIssueDetails
|
<PeekOverviewIssueDetails
|
||||||
|
setShowAlert={setShowAlert}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
issueReactions={issueReactions}
|
issueReactions={issueReactions}
|
||||||
|
@ -6,12 +6,13 @@ import { IssueService } from "services/issue/issue.service";
|
|||||||
// types
|
// types
|
||||||
import { TIssueGroupByOptions } from "types";
|
import { TIssueGroupByOptions } from "types";
|
||||||
import { IIssue } from "types/issues";
|
import { IIssue } from "types/issues";
|
||||||
import { IIssueResponse, TLoader, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues, ViewFlags } from "../../types";
|
import { IIssueResponse, TLoader, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues, ViewFlags, TIssueUpdateStatus } from "../../types";
|
||||||
import { RootStore } from "store/root";
|
import { RootStore } from "store/root";
|
||||||
|
|
||||||
export interface IProjectIssuesStore {
|
export interface IProjectIssuesStore {
|
||||||
// observable
|
// observable
|
||||||
loader: TLoader;
|
loader: TLoader;
|
||||||
|
isSubmitting: TIssueUpdateStatus;
|
||||||
issues: { [project_id: string]: IIssueResponse } | undefined;
|
issues: { [project_id: string]: IIssueResponse } | undefined;
|
||||||
// computed
|
// computed
|
||||||
getIssues: IIssueResponse | undefined;
|
getIssues: IIssueResponse | undefined;
|
||||||
@ -22,12 +23,14 @@ export interface IProjectIssuesStore {
|
|||||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<IIssue>) => Promise<IIssue>;
|
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<IIssue>) => Promise<IIssue>;
|
||||||
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
|
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
|
||||||
quickAddIssue: (workspaceSlug: string, projectId: string, data: IIssue) => Promise<IIssue>;
|
quickAddIssue: (workspaceSlug: string, projectId: string, data: IIssue) => Promise<IIssue>;
|
||||||
|
setIsSubmitting: (value: TIssueUpdateStatus) => void;
|
||||||
|
|
||||||
viewFlags: ViewFlags;
|
viewFlags: ViewFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProjectIssuesStore extends IssueBaseStore implements IProjectIssuesStore {
|
export class ProjectIssuesStore extends IssueBaseStore implements IProjectIssuesStore {
|
||||||
loader: TLoader = "init-loader";
|
loader: TLoader = "init-loader";
|
||||||
|
isSubmitting: TIssueUpdateStatus = "saved";
|
||||||
issues: { [project_id: string]: IIssueResponse } | undefined = undefined;
|
issues: { [project_id: string]: IIssueResponse } | undefined = undefined;
|
||||||
// root store
|
// root store
|
||||||
rootStore;
|
rootStore;
|
||||||
@ -48,6 +51,7 @@ export class ProjectIssuesStore extends IssueBaseStore implements IProjectIssues
|
|||||||
// observable
|
// observable
|
||||||
loader: observable.ref,
|
loader: observable.ref,
|
||||||
issues: observable.ref,
|
issues: observable.ref,
|
||||||
|
isSubmitting: observable.ref,
|
||||||
// computed
|
// computed
|
||||||
getIssues: computed,
|
getIssues: computed,
|
||||||
getIssuesIds: computed,
|
getIssuesIds: computed,
|
||||||
@ -57,6 +61,7 @@ export class ProjectIssuesStore extends IssueBaseStore implements IProjectIssues
|
|||||||
updateIssue: action,
|
updateIssue: action,
|
||||||
removeIssue: action,
|
removeIssue: action,
|
||||||
quickAddIssue: action,
|
quickAddIssue: action,
|
||||||
|
setIsSubmitting: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
@ -72,6 +77,10 @@ export class ProjectIssuesStore extends IssueBaseStore implements IProjectIssues
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsSubmitting = (value: "submitting" | "submitted" | "saved") => {
|
||||||
|
this.isSubmitting = value;
|
||||||
|
};
|
||||||
|
|
||||||
get getIssues() {
|
get getIssues() {
|
||||||
const projectId = this.rootStore?.project.projectId;
|
const projectId = this.rootStore?.project.projectId;
|
||||||
if (!projectId || !this.issues || !this.issues[projectId]) return undefined;
|
if (!projectId || !this.issues || !this.issues[projectId]) return undefined;
|
||||||
@ -157,11 +166,14 @@ export class ProjectIssuesStore extends IssueBaseStore implements IProjectIssues
|
|||||||
_issues[projectId][issueId] = { ..._issues[projectId][issueId], ...data };
|
_issues[projectId][issueId] = { ..._issues[projectId][issueId], ...data };
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
this.isSubmitting = "submitting";
|
||||||
this.issues = _issues;
|
this.issues = _issues;
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
|
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
|
||||||
|
|
||||||
|
this.isSubmitting = "submitted";
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.fetchIssues(workspaceSlug, projectId, "mutation");
|
this.fetchIssues(workspaceSlug, projectId, "mutation");
|
||||||
|
@ -31,3 +31,6 @@ export interface ViewFlags {
|
|||||||
enableIssueCreation: boolean;
|
enableIssueCreation: boolean;
|
||||||
enableInlineEditing: boolean;
|
enableInlineEditing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// issue update status
|
||||||
|
export type TIssueUpdateStatus = "saved" | "submitting" | "submitted";
|
||||||
|
Loading…
Reference in New Issue
Block a user