UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: карточка внешнего контура
This commit is contained in:
parent
347d95709c
commit
86b17b23c9
|
|
@ -7,16 +7,13 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { MoveDiagonal, MoveRight } from "lucide-react";
|
||||
import { Check, MoveDiagonal, MoveRight, X } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IconButton } from "@plane/propel/icon-button";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import {
|
||||
CenterPanelIcon,
|
||||
CheckCircleFilledIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
CloseCircleFilledIcon,
|
||||
CopyLinkIcon,
|
||||
FullScreenPanelIcon,
|
||||
SidePanelIcon,
|
||||
|
|
@ -100,6 +97,7 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont
|
|||
contourRequest.capabilities?.can_source_decide ??
|
||||
(contourRequest.status === "closed" && contourRequest.source_decision !== "accepted");
|
||||
const isSourceAccepted = contourRequest.source_decision === "accepted";
|
||||
const isDecisionSubmitting = loader === "mutation-loading";
|
||||
|
||||
const redirectToRelativeIssue = useCallback(
|
||||
(direction: "next" | "prev") => {
|
||||
|
|
@ -291,16 +289,30 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont
|
|||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{canReviewClosedRequest && (
|
||||
<>
|
||||
<Button variant="primary" size="lg" onClick={() => handleDecision("accept")} className="nodedc-external-primary-button">
|
||||
<CheckCircleFilledIcon className="size-4 shrink-0 text-success-secondary" />
|
||||
{t("external_contours_page.actions.accept")}
|
||||
</Button>
|
||||
<Button variant="secondary" size="lg" onClick={() => setIsDeclineModalOpen(true)} className="nodedc-external-action-button">
|
||||
<CloseCircleFilledIcon className="size-4 shrink-0 text-danger-secondary" />
|
||||
{t("external_contours_page.actions.decline")}
|
||||
</Button>
|
||||
</>
|
||||
<div className="nodedc-external-decision-cluster">
|
||||
<Tooltip tooltipContent={t("external_contours_page.actions.accept")}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("external_contours_page.actions.accept")}
|
||||
onClick={() => handleDecision("accept")}
|
||||
disabled={isDecisionSubmitting}
|
||||
className="nodedc-external-decision-button nodedc-external-decision-button-accept"
|
||||
>
|
||||
<Check className="size-4" strokeWidth={2.6} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip tooltipContent={t("external_contours_page.actions.decline")}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("external_contours_page.actions.decline")}
|
||||
onClick={() => setIsDeclineModalOpen(true)}
|
||||
disabled={isDecisionSubmitting}
|
||||
className="nodedc-external-decision-button nodedc-external-decision-button-decline"
|
||||
>
|
||||
<X className="size-4" strokeWidth={2.5} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSourceAccepted && (
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ import { useTranslation } from "@plane/i18n";
|
|||
import { LabelPropertyIcon, PriorityIcon, PriorityPropertyIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { TExternalContourRequest, TIssue, TNameDescriptionLoader } from "@plane/types";
|
||||
import { EFileAssetType } from "@plane/types";
|
||||
import { EFileAssetType, EIssueServiceType } from "@plane/types";
|
||||
import { getTextContent } from "@plane/utils";
|
||||
import { DescriptionVersionsRoot } from "@/components/core/description-versions";
|
||||
import { DescriptionInput } from "@/components/editor/rich-text/description-input";
|
||||
import { DescriptionInputLoader } from "@/components/editor/rich-text/description-input/loader";
|
||||
import { IssueAttachmentRoot } from "@/components/issues/attachment";
|
||||
import type { TIssueOperations } from "@/components/issues/issue-detail";
|
||||
import { IssueActivity } from "@/components/issues/issue-detail/issue-activity";
|
||||
import { IssueDetailWidgets } from "@/components/issues/issue-detail-widgets";
|
||||
import { IssueReaction } from "@/components/issues/issue-detail/reactions";
|
||||
import { IssueTitleInput } from "@/components/issues/title-input";
|
||||
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
|
||||
|
|
@ -329,8 +329,16 @@ export const ExternalContoursIssueMainContent = observer(function ExternalContou
|
|||
|
||||
<ExternalContoursRequestTraceability contourRequest={contourRequest} />
|
||||
|
||||
<div className="nodedc-external-section overflow-visible px-4 py-4">
|
||||
<IssueAttachmentRoot workspaceSlug={workspaceSlug} projectId={targetProjectId} issueId={issue.id} disabled={!isEditable} />
|
||||
<div className="nodedc-external-section nodedc-external-detail-widgets overflow-visible px-4 py-4">
|
||||
<IssueDetailWidgets
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={targetProjectId}
|
||||
issueId={issue.id}
|
||||
disabled={!isEditable}
|
||||
issueOperations={issueOperations}
|
||||
issueServiceType={EIssueServiceType.ISSUES}
|
||||
compactView
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-external-section overflow-visible px-4 py-4">
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import usePeekOverviewOutsideClickDetector from "@/hooks/use-peek-overview-outsi
|
|||
import { ExternalContoursIssueActionsHeader, type TExternalContourPeekMode } from "./issue-header";
|
||||
|
||||
const SIDE_PEEK_WIDTH_STORAGE_KEY = "nodedc:external-contour-peek-width";
|
||||
const SIDE_PEEK_MIN_WIDTH = 780;
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -41,9 +42,9 @@ export const ExternalContoursPeekShell = observer(function ExternalContoursPeekS
|
|||
} = props;
|
||||
const [peekMode, setPeekMode] = useState<TExternalContourPeekMode>("side-peek");
|
||||
const [sidePeekWidth, setSidePeekWidth] = useState<number>(() => {
|
||||
if (typeof window === "undefined") return 720;
|
||||
if (typeof window === "undefined") return SIDE_PEEK_MIN_WIDTH;
|
||||
|
||||
const fallbackWidth = Math.max(640, Math.floor(window.innerWidth * 0.5));
|
||||
const fallbackWidth = Math.max(SIDE_PEEK_MIN_WIDTH, Math.floor(window.innerWidth * 0.54));
|
||||
const storedWidth = window.localStorage.getItem(SIDE_PEEK_WIDTH_STORAGE_KEY);
|
||||
const parsedWidth = storedWidth ? parseInt(storedWidth, 10) : NaN;
|
||||
|
||||
|
|
@ -72,10 +73,9 @@ export const ExternalContoursPeekShell = observer(function ExternalContoursPeekS
|
|||
(event: MouseEvent) => {
|
||||
if (!isResizingPeek) return;
|
||||
|
||||
const maxWidth = Math.max(720, window.innerWidth - 48);
|
||||
const minWidth = 640;
|
||||
const maxWidth = Math.max(SIDE_PEEK_MIN_WIDTH, window.innerWidth - 48);
|
||||
const deltaX = event.clientX - initialMouseXRef.current;
|
||||
const nextWidth = Math.min(Math.max(initialPeekWidthRef.current - deltaX, minWidth), maxWidth);
|
||||
const nextWidth = Math.min(Math.max(initialPeekWidthRef.current - deltaX, SIDE_PEEK_MIN_WIDTH), maxWidth);
|
||||
setSidePeekWidth(nextWidth);
|
||||
},
|
||||
[isResizingPeek]
|
||||
|
|
@ -94,7 +94,7 @@ export const ExternalContoursPeekShell = observer(function ExternalContoursPeekS
|
|||
|
||||
useEffect(() => {
|
||||
const handleWindowResize = () => {
|
||||
const maxWidth = Math.max(720, window.innerWidth - 48);
|
||||
const maxWidth = Math.max(SIDE_PEEK_MIN_WIDTH, window.innerWidth - 48);
|
||||
setSidePeekWidth((currentWidth) => Math.min(currentWidth, maxWidth));
|
||||
};
|
||||
|
||||
|
|
@ -105,8 +105,8 @@ export const ExternalContoursPeekShell = observer(function ExternalContoursPeekS
|
|||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const maxWidth = Math.max(720, window.innerWidth - 48);
|
||||
const clampedWidth = Math.min(Math.max(sidePeekWidth, 640), maxWidth);
|
||||
const maxWidth = Math.max(SIDE_PEEK_MIN_WIDTH, window.innerWidth - 48);
|
||||
const clampedWidth = Math.min(Math.max(sidePeekWidth, SIDE_PEEK_MIN_WIDTH), maxWidth);
|
||||
|
||||
window.localStorage.setItem(SIDE_PEEK_WIDTH_STORAGE_KEY, String(clampedWidth));
|
||||
|
||||
|
|
@ -151,7 +151,7 @@ export const ExternalContoursPeekShell = observer(function ExternalContoursPeekS
|
|||
? "absolute z-[25] flex flex-col overflow-hidden border border-subtle/70 bg-surface-1/80 backdrop-blur-2xl transition-all duration-300"
|
||||
: "h-full w-full",
|
||||
!embedIssue && {
|
||||
"top-3 right-3 bottom-3 w-[calc(100%-1.5rem)] rounded-[28px] border md:min-w-[640px] md:max-w-[calc(100vw-1.5rem)]":
|
||||
"top-3 right-3 bottom-3 w-[calc(100%-1.5rem)] rounded-[28px] border md:min-w-[780px] md:max-w-[calc(100vw-1.5rem)]":
|
||||
peekMode === "side-peek",
|
||||
"top-[8.33%] left-[8.33%] size-5/6 rounded-[28px]": peekMode === "modal",
|
||||
"absolute inset-0 m-4 rounded-[28px]": peekMode === "full-screen",
|
||||
|
|
|
|||
|
|
@ -3603,11 +3603,16 @@
|
|||
|
||||
.nodedc-external-detail-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.nodedc-external-detail-toolbar > .flex {
|
||||
flex-wrap: nowrap !important;
|
||||
}
|
||||
|
||||
.nodedc-external-toolbar-cluster {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -3618,6 +3623,85 @@
|
|||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.018) !important;
|
||||
}
|
||||
|
||||
.nodedc-external-decision-cluster {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
min-height: 3.05rem;
|
||||
padding: 0.28rem !important;
|
||||
border: 0 !important;
|
||||
outline: none !important;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.055) !important;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.018),
|
||||
0 10px 28px rgba(0, 0, 0, 0.1) !important;
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.nodedc-external-decision-button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 2.5rem;
|
||||
min-width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border: 0 !important;
|
||||
outline: none !important;
|
||||
border-radius: 999px;
|
||||
padding: 0 !important;
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
background 160ms ease,
|
||||
color 160ms ease,
|
||||
opacity 160ms ease;
|
||||
}
|
||||
|
||||
.nodedc-external-decision-button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.nodedc-external-decision-button:disabled {
|
||||
cursor: progress;
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.nodedc-external-decision-button-accept {
|
||||
background: rgb(var(--nodedc-card-active-rgb)) !important;
|
||||
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.18),
|
||||
0 8px 18px rgba(var(--nodedc-card-active-rgb), 0.2) !important;
|
||||
}
|
||||
|
||||
.nodedc-external-decision-button-accept:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 86%, white) !important;
|
||||
}
|
||||
|
||||
.nodedc-external-decision-button-decline {
|
||||
background: rgba(255, 255, 255, 0.12) !important;
|
||||
color: rgba(255, 255, 255, 0.72) !important;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04) !important;
|
||||
}
|
||||
|
||||
.nodedc-external-decision-button-decline:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.18) !important;
|
||||
color: rgba(255, 255, 255, 0.86) !important;
|
||||
}
|
||||
|
||||
.nodedc-external-detail-widgets {
|
||||
--nodedc-widget-surface-rgb: 255 255 255;
|
||||
}
|
||||
|
||||
.nodedc-external-detail-widgets [data-slot="button"],
|
||||
.nodedc-external-detail-widgets button {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.nodedc-external-detail-widgets .nodedc-attachment-upload {
|
||||
min-height: 4.5rem;
|
||||
}
|
||||
|
||||
.nodedc-external-priority-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
Loading…
Reference in New Issue