ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: причина отклонения, reply и in-app уведомления

This commit is contained in:
DCCONSTRUCTIONS
2026-04-19 09:22:15 +03:00
parent 61a8625a5c
commit 0a584abf26
25 changed files with 623 additions and 32 deletions
@@ -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>
);
});