mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: issue update status in detail page
This commit is contained in:
parent
9d7b0c6daa
commit
85d295b21d
@ -1,5 +1,6 @@
|
|||||||
import { ChangeEvent, FC, useCallback, useEffect, useState } from "react";
|
import { ChangeEvent, FC, useCallback, useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
@ -33,11 +34,14 @@ const fileService = new FileService();
|
|||||||
export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
||||||
const { issue, handleFormSubmit, workspaceSlug, isAllowed } = props;
|
const { issue, handleFormSubmit, workspaceSlug, isAllowed } = props;
|
||||||
// states
|
// states
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
|
||||||
const [characterLimit, setCharacterLimit] = useState(false);
|
const [characterLimit, setCharacterLimit] = useState(false);
|
||||||
|
|
||||||
const { setShowAlert } = useReloadConfirmations();
|
// mobx store
|
||||||
|
const {
|
||||||
|
projectIssues: { setIsSubmitting },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const { setShowAlert } = useReloadConfirmations();
|
||||||
const editorSuggestion = useEditorSuggestions();
|
const editorSuggestion = useEditorSuggestions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -73,17 +77,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
|||||||
[handleFormSubmit]
|
[handleFormSubmit]
|
||||||
);
|
);
|
||||||
|
|
||||||
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;
|
||||||
@ -166,13 +159,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -25,20 +25,10 @@ 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 {
|
const { workspaceSlug, issue, issueReactions, user, issueUpdate, issueReactionCreate, issueReactionRemove } = props;
|
||||||
workspaceSlug,
|
|
||||||
issue,
|
|
||||||
issueReactions,
|
|
||||||
user,
|
|
||||||
issueUpdate,
|
|
||||||
issueReactionCreate,
|
|
||||||
issueReactionRemove,
|
|
||||||
setShowAlert,
|
|
||||||
} = props;
|
|
||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
user: userStore,
|
user: userStore,
|
||||||
@ -47,10 +37,10 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
const { currentProjectRole } = userStore;
|
const { currentProjectRole } = userStore;
|
||||||
const isAllowed = [15, 20].includes(currentProjectRole || 0);
|
const isAllowed = [15, 20].includes(currentProjectRole || 0);
|
||||||
// states
|
// states
|
||||||
|
|
||||||
const [characterLimit, setCharacterLimit] = useState(false);
|
const [characterLimit, setCharacterLimit] = useState(false);
|
||||||
// hooks
|
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
const { setShowAlert } = useReloadConfirmations();
|
||||||
const editorSuggestions = useEditorSuggestions();
|
const editorSuggestions = useEditorSuggestions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -287,7 +287,6 @@ 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}
|
||||||
@ -322,7 +321,6 @@ 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}
|
||||||
|
@ -19,9 +19,9 @@ import {
|
|||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
import { SubIssuesRoot } from "./sub-issues";
|
import { SubIssuesRoot } from "./sub-issues";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, LayersIcon } from "@plane/ui";
|
import { CustomMenu, LayersIcon, StateGroupIcon } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { MinusCircle } from "lucide-react";
|
import { MinusCircle, RefreshCw } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueComment } from "types";
|
import { IIssue, IIssueComment } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -45,11 +45,20 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { user: userStore, project: projectStore } = useMobxStore();
|
const {
|
||||||
|
user: userStore,
|
||||||
|
project: projectStore,
|
||||||
|
projectState: { states },
|
||||||
|
projectIssues: { isSubmitting },
|
||||||
|
} = useMobxStore();
|
||||||
const user = userStore.currentUser ?? undefined;
|
const user = userStore.currentUser ?? undefined;
|
||||||
const userRole = userStore.currentProjectRole;
|
const userRole = userStore.currentProjectRole;
|
||||||
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : undefined;
|
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : undefined;
|
||||||
|
|
||||||
|
const currentIssueState = projectId
|
||||||
|
? states[projectId.toString()]?.find((s) => s.id === issueDetails.state)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const { data: siblingIssues } = useSWR(
|
const { data: siblingIssues } = useSWR(
|
||||||
workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null,
|
workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null,
|
||||||
workspaceSlug && projectId && issueDetails?.parent
|
workspaceSlug && projectId && issueDetails?.parent
|
||||||
@ -162,6 +171,29 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
<div className="flex items-center mb-5">
|
||||||
|
{currentIssueState && (
|
||||||
|
<StateGroupIcon
|
||||||
|
className="h-4 w-4 mr-3"
|
||||||
|
stateGroup={currentIssueState.group}
|
||||||
|
color={currentIssueState.color}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<h4 className="text-lg text-custom-text-300 font-medium mr-4">
|
||||||
|
{issueDetails?.project_detail?.identifier}-{issueDetails?.sequence_id}
|
||||||
|
</h4>
|
||||||
|
<div
|
||||||
|
className={`flex transition-all 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<IssueDescriptionForm
|
<IssueDescriptionForm
|
||||||
workspaceSlug={workspaceSlug as string}
|
workspaceSlug={workspaceSlug as string}
|
||||||
issue={issueDetails}
|
issue={issueDetails}
|
||||||
|
@ -33,7 +33,7 @@ import {
|
|||||||
import { CustomDatePicker } from "components/ui";
|
import { CustomDatePicker } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { Bell, CalendarDays, LinkIcon, Plus, Signal, Tag, Trash2, Triangle, User2 } from "lucide-react";
|
import { Bell, CalendarDays, LinkIcon, Plus, Signal, Tag, Trash2, Triangle, User2 } from "lucide-react";
|
||||||
import { Button, ContrastIcon, DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
import { Button, ContrastIcon, DiceIcon, DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
@ -79,7 +79,10 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const [linkModal, setLinkModal] = useState(false);
|
const [linkModal, setLinkModal] = useState(false);
|
||||||
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
|
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
|
||||||
|
|
||||||
const { user: userStore } = useMobxStore();
|
const {
|
||||||
|
user: userStore,
|
||||||
|
projectState: { states },
|
||||||
|
} = useMobxStore();
|
||||||
const user = userStore.currentUser;
|
const user = userStore.currentUser;
|
||||||
const userRole = userStore.currentProjectRole;
|
const userRole = userStore.currentProjectRole;
|
||||||
|
|
||||||
@ -247,6 +250,10 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const isNotAllowed = userRole === 5 || userRole === 10;
|
const isNotAllowed = userRole === 5 || userRole === 10;
|
||||||
|
|
||||||
|
const currentIssueState = projectId
|
||||||
|
? states[projectId.toString()]?.find((s) => s.id === issueDetail?.state)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LinkModal
|
<LinkModal
|
||||||
@ -265,9 +272,19 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
<div className="h-full w-full flex flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
|
<div className="h-full w-full flex flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
|
||||||
<div className="flex items-center justify-between px-5 pb-3">
|
<div className="flex items-center justify-between px-5 pb-3">
|
||||||
<h4 className="text-sm font-medium">
|
<div className="flex items-center gap-x-2">
|
||||||
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
|
{currentIssueState && (
|
||||||
</h4>
|
<StateGroupIcon
|
||||||
|
className="h-4 w-4"
|
||||||
|
stateGroup={currentIssueState.group}
|
||||||
|
color={currentIssueState.color}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<h4 className="text-lg text-custom-text-300 font-medium">
|
||||||
|
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{issueDetail?.created_by !== user?.id &&
|
{issueDetail?.created_by !== user?.id &&
|
||||||
!issueDetail?.assignees.includes(user?.id ?? "") &&
|
!issueDetail?.assignees.includes(user?.id ?? "") &&
|
||||||
|
@ -2,6 +2,8 @@ import React, { useCallback, useEffect, ReactElement } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
import { observer } from "mobx-react-lite/dist/observer";
|
||||||
// services
|
// services
|
||||||
import { IssueService } from "services/issue";
|
import { IssueService } from "services/issue";
|
||||||
// layouts
|
// layouts
|
||||||
@ -9,6 +11,8 @@ import { AppLayout } from "layouts/app-layout";
|
|||||||
// components
|
// components
|
||||||
import { ProjectIssueDetailsHeader } from "components/headers";
|
import { ProjectIssueDetailsHeader } from "components/headers";
|
||||||
import { IssueDetailsSidebar, IssueMainContent } from "components/issues";
|
import { IssueDetailsSidebar, IssueMainContent } from "components/issues";
|
||||||
|
// hooks
|
||||||
|
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState } from "components/common";
|
import { EmptyState } from "components/common";
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
@ -36,11 +40,18 @@ const defaultValues: Partial<IIssue> = {
|
|||||||
// services
|
// services
|
||||||
const issueService = new IssueService();
|
const issueService = new IssueService();
|
||||||
|
|
||||||
const IssueDetailsPage: NextPageWithLayout = () => {
|
const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
|
|
||||||
|
// mobx store
|
||||||
|
const {
|
||||||
|
projectIssues: { isSubmitting, setIsSubmitting },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const { setShowAlert } = useReloadConfirmations();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: issueDetails,
|
data: issueDetails,
|
||||||
mutate: mutateIssueDetails,
|
mutate: mutateIssueDetails,
|
||||||
@ -60,6 +71,7 @@ const IssueDetailsPage: NextPageWithLayout = () => {
|
|||||||
async (formData: Partial<IIssue>) => {
|
async (formData: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
if (!workspaceSlug || !projectId || !issueId) return;
|
||||||
|
|
||||||
|
setIsSubmitting("submitting");
|
||||||
mutate<IIssue>(
|
mutate<IIssue>(
|
||||||
ISSUE_DETAILS(issueId as string),
|
ISSUE_DETAILS(issueId as string),
|
||||||
(prevData) => {
|
(prevData) => {
|
||||||
@ -85,6 +97,7 @@ const IssueDetailsPage: NextPageWithLayout = () => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
mutateIssueDetails();
|
mutateIssueDetails();
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
||||||
|
setIsSubmitting("submitted");
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -102,6 +115,17 @@ const IssueDetailsPage: NextPageWithLayout = () => {
|
|||||||
});
|
});
|
||||||
}, [issueDetails, reset, issueId]);
|
}, [issueDetails, reset, issueId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSubmitting === "submitted") {
|
||||||
|
setShowAlert(false);
|
||||||
|
setTimeout(async () => {
|
||||||
|
setIsSubmitting("saved");
|
||||||
|
}, 2000);
|
||||||
|
} else if (isSubmitting === "submitting") {
|
||||||
|
setShowAlert(true);
|
||||||
|
}
|
||||||
|
}, [isSubmitting, setShowAlert]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
@ -147,7 +171,7 @@ const IssueDetailsPage: NextPageWithLayout = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
IssueDetailsPage.getLayout = function getLayout(page: ReactElement) {
|
IssueDetailsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
Loading…
Reference in New Issue
Block a user