ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: причина отклонения, reply и in-app уведомления
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { EModalPosition, EModalWidth, ModalCore, TextArea } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
isSubmitting?: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (comment: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export function ExternalContourDeclineModal(props: Props) {
|
||||
const { isOpen, isSubmitting = false, onClose, onSubmit } = props;
|
||||
const { t } = useTranslation();
|
||||
const [comment, setComment] = useState("");
|
||||
|
||||
const handleClose = () => {
|
||||
if (isSubmitting) return;
|
||||
setComment("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!comment.trim() || isSubmitting) return;
|
||||
await onSubmit(comment.trim());
|
||||
setComment("");
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalCore
|
||||
isOpen={isOpen}
|
||||
handleClose={handleClose}
|
||||
position={EModalPosition.CENTER}
|
||||
width={EModalWidth.LG}
|
||||
className="rounded-lg"
|
||||
>
|
||||
<div className="space-y-4 p-6">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-18 font-semibold text-primary">{t("external_contours_page.decline_modal.title")}</h3>
|
||||
<p className="text-13 text-secondary">{t("external_contours_page.decline_modal.description")}</p>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
placeholder={t("external_contours_page.decline_modal.placeholder")}
|
||||
rows={5}
|
||||
disabled={isSubmitting}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="secondary" onClick={handleClose} disabled={isSubmitting}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSubmit} disabled={isSubmitting || !comment.trim()}>
|
||||
{t("external_contours_page.decline_modal.submit")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalCore>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { PanelLeft } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
@@ -22,6 +22,7 @@ import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { ExternalContourStatePill } from "./state-pill";
|
||||
import { ExternalContourDeclineModal } from "./decline-modal";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
@@ -45,7 +46,8 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
const router = useAppRouter();
|
||||
const { currentTab, decideRequest, filteredRequestIds, handleCurrentTab } = useProjectExternalContours();
|
||||
const [isDeclineModalOpen, setIsDeclineModalOpen] = useState(false);
|
||||
const { currentTab, decideRequest, filteredRequestIds, handleCurrentTab, loader } = useProjectExternalContours();
|
||||
const { getProjectById } = useProject();
|
||||
|
||||
const issue = contourRequest.issue;
|
||||
@@ -96,10 +98,11 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont
|
||||
})
|
||||
);
|
||||
|
||||
const handleDecision = async (action: "accept" | "decline") => {
|
||||
const handleDecision = async (action: "accept" | "decline", comment?: string) => {
|
||||
try {
|
||||
await decideRequest(workspaceSlug, sourceProjectId, contourRequest.id, action);
|
||||
await decideRequest(workspaceSlug, sourceProjectId, contourRequest.id, action, comment);
|
||||
if (action === "decline") {
|
||||
setIsDeclineModalOpen(false);
|
||||
await handleCurrentTab(workspaceSlug, sourceProjectId, EInboxIssueCurrentTab.OPEN);
|
||||
router.push(
|
||||
`/${workspaceSlug}/projects/${sourceProjectId}/external-contours?currentTab=${EInboxIssueCurrentTab.OPEN}&inboxIssueId=${contourRequest.id}`
|
||||
@@ -125,6 +128,13 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExternalContourDeclineModal
|
||||
isOpen={isDeclineModalOpen}
|
||||
isSubmitting={loader === "mutation-loading"}
|
||||
onClose={() => setIsDeclineModalOpen(false)}
|
||||
onSubmit={(comment) => handleDecision("decline", comment)}
|
||||
/>
|
||||
|
||||
<Row className="relative z-15 hidden h-full w-full items-center justify-between gap-2 border-b border-subtle bg-surface-1 lg:flex">
|
||||
<div className="flex items-center gap-4">
|
||||
{issue?.project_id && issue.sequence_id && (
|
||||
@@ -151,7 +161,7 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont
|
||||
<CheckCircleFilledIcon className="size-4 shrink-0 text-success-secondary" />
|
||||
{t("external_contours_page.actions.accept")}
|
||||
</Button>
|
||||
<Button variant="secondary" size="lg" onClick={() => handleDecision("decline")}>
|
||||
<Button variant="secondary" size="lg" onClick={() => setIsDeclineModalOpen(true)}>
|
||||
<CloseCircleFilledIcon className="size-4 shrink-0 text-danger-secondary" />
|
||||
{t("external_contours_page.actions.decline")}
|
||||
</Button>
|
||||
@@ -189,7 +199,7 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont
|
||||
<Button variant="secondary" size="sm" onClick={() => handleDecision("accept")}>
|
||||
{t("external_contours_page.actions.accept")}
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => handleDecision("decline")}>
|
||||
<Button variant="secondary" size="sm" onClick={() => setIsDeclineModalOpen(true)}>
|
||||
{t("external_contours_page.actions.decline")}
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@@ -34,6 +34,7 @@ import { ExternalContoursMirroredAttachments } from "./mirrored-attachments";
|
||||
import { ExternalContoursMirroredComments } from "./mirrored-comments";
|
||||
import { ExternalContoursIssueContentProperties } from "./issue-properties";
|
||||
import { ExternalContoursRequestTraceability } from "./request-traceability";
|
||||
import { ExternalContoursSourceReplyBox } from "./source-reply-box";
|
||||
|
||||
const workItemVersionService = new WorkItemVersionService();
|
||||
const issueService = new IssueService();
|
||||
@@ -172,6 +173,12 @@ export const ExternalContoursIssueMainContent = observer(function ExternalContou
|
||||
|
||||
<ExternalContoursMirroredAttachments attachments={mirroredAttachments} />
|
||||
|
||||
<ExternalContoursSourceReplyBox
|
||||
workspaceSlug={workspaceSlug}
|
||||
sourceProjectId={sourceProjectId}
|
||||
requestId={contourRequest.id}
|
||||
/>
|
||||
|
||||
<ExternalContoursMirroredComments comments={mirroredComments} />
|
||||
|
||||
<ExternalContoursMirroredActivity activity={mirroredActivity} />
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { TextArea } from "@plane/ui";
|
||||
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
sourceProjectId: string;
|
||||
requestId: string;
|
||||
};
|
||||
|
||||
export const ExternalContoursSourceReplyBox = observer(function ExternalContoursSourceReplyBox(props: Props) {
|
||||
const { workspaceSlug, sourceProjectId, requestId } = props;
|
||||
const { t } = useTranslation();
|
||||
const { loader, replyToRequest } = useProjectExternalContours();
|
||||
const [comment, setComment] = useState("");
|
||||
|
||||
const isSubmitting = loader === "mutation-loading";
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!comment.trim() || isSubmitting) return;
|
||||
|
||||
try {
|
||||
await replyToRequest(workspaceSlug, sourceProjectId, requestId, comment.trim());
|
||||
setComment("");
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("success"),
|
||||
message: t("external_contours_page.reply.success"),
|
||||
});
|
||||
} catch (error: any) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("error"),
|
||||
message: error?.error || t("external_contours_page.reply.error"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3 py-4">
|
||||
<div className="text-body-sm-medium">{t("external_contours_page.reply.title")}</div>
|
||||
<TextArea
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
rows={4}
|
||||
placeholder={t("external_contours_page.reply.placeholder")}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button variant="primary" onClick={handleSubmit} disabled={isSubmitting || !comment.trim()}>
|
||||
{t("external_contours_page.reply.submit")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -20,6 +20,7 @@ import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties";
|
||||
// plane web imports
|
||||
import { useNotificationPreview } from "@/plane-web/hooks/use-notification-preview";
|
||||
import { ExternalContoursContentRoot } from "@/plane-web/components/projects/external-contours/content-root";
|
||||
// local imports
|
||||
import { InboxContentRoot } from "../inbox/content";
|
||||
|
||||
@@ -40,7 +41,7 @@ export const NotificationsRoot = observer(function NotificationsRoot({ workspace
|
||||
const { fetchUserProjectInfo } = useUserPermissions();
|
||||
const { isWorkItem, PeekOverviewComponent, setPeekWorkItem } = useNotificationPreview();
|
||||
// derived values
|
||||
const { workspace_slug, project_id, issue_id, is_inbox_issue } =
|
||||
const { workspace_slug, project_id, issue_id, is_inbox_issue, is_external_contour } =
|
||||
notificationLiteByNotificationId(currentSelectedNotificationId);
|
||||
|
||||
// fetching workspace work item properties
|
||||
@@ -64,10 +65,12 @@ export const NotificationsRoot = observer(function NotificationsRoot({ workspace
|
||||
|
||||
// fetching user project member info
|
||||
const { isLoading: projectMemberInfoLoader } = useSWR(
|
||||
workspace_slug && project_id && is_inbox_issue
|
||||
workspace_slug && project_id && (is_inbox_issue || is_external_contour)
|
||||
? `PROJECT_MEMBER_PERMISSION_INFO_${workspace_slug}_${project_id}`
|
||||
: null,
|
||||
workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null
|
||||
workspace_slug && project_id && (is_inbox_issue || is_external_contour)
|
||||
? () => fetchUserProjectInfo(workspace_slug, project_id)
|
||||
: null
|
||||
);
|
||||
|
||||
const embedRemoveCurrentNotification = useCallback(
|
||||
@@ -91,7 +94,23 @@ export const NotificationsRoot = observer(function NotificationsRoot({ workspace
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{is_inbox_issue === true && workspace_slug && project_id && issue_id ? (
|
||||
{is_external_contour === true && workspace_slug && project_id && issue_id ? (
|
||||
<>
|
||||
{projectMemberInfoLoader ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
) : (
|
||||
<ExternalContoursContentRoot
|
||||
setIsMobileSidebar={() => {}}
|
||||
isMobileSidebar={false}
|
||||
workspaceSlug={workspace_slug}
|
||||
projectId={project_id}
|
||||
inboxIssueId={issue_id}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : is_inbox_issue === true && workspace_slug && project_id && issue_id ? (
|
||||
<>
|
||||
{projectMemberInfoLoader ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
|
||||
@@ -81,10 +81,27 @@ export class ExternalContourService extends APIService {
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
requestId: string,
|
||||
action: "accept" | "decline"
|
||||
action: "accept" | "decline",
|
||||
comment?: string
|
||||
): Promise<TExternalContourRequest> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/external-contours/${requestId}/decision/`, {
|
||||
action,
|
||||
comment,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async reply(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
requestId: string,
|
||||
comment: string
|
||||
): Promise<TExternalContourRequest> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/external-contours/${requestId}/reply/`, {
|
||||
comment,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
|
||||
@@ -38,7 +38,14 @@ export interface IProjectExternalContoursStore {
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
requestId: string,
|
||||
action: "accept" | "decline"
|
||||
action: "accept" | "decline",
|
||||
comment?: string
|
||||
) => Promise<TExternalContourRequest | undefined>;
|
||||
replyToRequest: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
requestId: string,
|
||||
comment: string
|
||||
) => Promise<TExternalContourRequest | undefined>;
|
||||
fetchTargetProjects: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
fetchTargetOptions: (workspaceSlug: string, projectId: string, targetProjectId: string) => Promise<TExternalContourTargetOptions | undefined>;
|
||||
@@ -89,6 +96,7 @@ export class ProjectExternalContoursStore implements IProjectExternalContoursSto
|
||||
fetchRequestById: action,
|
||||
createRequest: action,
|
||||
decideRequest: action,
|
||||
replyToRequest: action,
|
||||
handleCurrentTab: action,
|
||||
upsertRequests: action,
|
||||
updateRequestIssue: action,
|
||||
@@ -231,11 +239,29 @@ export class ProjectExternalContoursStore implements IProjectExternalContoursSto
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
requestId: string,
|
||||
action: "accept" | "decline"
|
||||
action: "accept" | "decline",
|
||||
comment?: string
|
||||
) => {
|
||||
this.loader = "mutation-loading";
|
||||
try {
|
||||
const request = await this.externalContourService.decide(workspaceSlug, projectId, requestId, action);
|
||||
const request = await this.externalContourService.decide(workspaceSlug, projectId, requestId, action, comment);
|
||||
runInAction(() => {
|
||||
this.upsertRequests([request]);
|
||||
this.loader = undefined;
|
||||
});
|
||||
return request;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.loader = undefined;
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
replyToRequest = async (workspaceSlug: string, projectId: string, requestId: string, comment: string) => {
|
||||
this.loader = "mutation-loading";
|
||||
try {
|
||||
const request = await this.externalContourService.reply(workspaceSlug, projectId, requestId, comment);
|
||||
runInAction(() => {
|
||||
this.upsertRequests([request]);
|
||||
this.loader = undefined;
|
||||
|
||||
@@ -48,6 +48,7 @@ export class Notification implements INotification {
|
||||
archived_at: string | undefined = undefined;
|
||||
snoozed_till: string | undefined = undefined;
|
||||
is_inbox_issue: boolean | undefined = undefined;
|
||||
is_external_contour: boolean | undefined = undefined;
|
||||
is_mentioned_notification: boolean | undefined = undefined;
|
||||
workspace: string | undefined = undefined;
|
||||
project: string | undefined = undefined;
|
||||
@@ -79,6 +80,7 @@ export class Notification implements INotification {
|
||||
archived_at: observable.ref,
|
||||
snoozed_till: observable.ref,
|
||||
is_inbox_issue: observable.ref,
|
||||
is_external_contour: observable.ref,
|
||||
is_mentioned_notification: observable.ref,
|
||||
workspace: observable.ref,
|
||||
project: observable.ref,
|
||||
@@ -112,6 +114,7 @@ export class Notification implements INotification {
|
||||
this.archived_at = this.notification.archived_at;
|
||||
this.snoozed_till = this.notification.snoozed_till;
|
||||
this.is_inbox_issue = this.notification.is_inbox_issue;
|
||||
this.is_external_contour = this.notification.is_external_contour;
|
||||
this.is_mentioned_notification = this.notification.is_mentioned_notification;
|
||||
this.workspace = this.notification.workspace;
|
||||
this.project = this.notification.project;
|
||||
@@ -143,6 +146,7 @@ export class Notification implements INotification {
|
||||
archived_at: this.archived_at,
|
||||
snoozed_till: this.snoozed_till,
|
||||
is_inbox_issue: this.is_inbox_issue,
|
||||
is_external_contour: this.is_external_contour,
|
||||
is_mentioned_notification: this.is_mentioned_notification,
|
||||
workspace: this.workspace,
|
||||
project: this.project,
|
||||
|
||||
@@ -169,6 +169,7 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
|
||||
notification_id: notification.id,
|
||||
issue_id: notification.data?.issue?.id,
|
||||
is_inbox_issue: notification.is_inbox_issue || false,
|
||||
is_external_contour: notification.is_external_contour || false,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user