UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: auth screens, error pages и новый канон внешних контуров

This commit is contained in:
DCCONSTRUCTIONS
2026-04-20 10:05:31 +03:00
parent df68c96795
commit c6ace8b9cc
25 changed files with 564 additions and 360 deletions
@@ -29,13 +29,13 @@ export const ExternalContoursIssueContentProperties = observer(function External
if (!issue || !issue?.id) return <></>;
return (
<div className="flex w-full flex-col divide-y-2 divide-subtle-1">
<div className="nodedc-external-section flex w-full flex-col px-4 py-4">
<div className="w-full overflow-y-auto">
<h5 className="mb-2 text-body-sm-medium">{t("external_contours_page.properties.section_title")}</h5>
<div className={`divide-y-2 divide-subtle-1 ${!isEditable ? "opacity-60" : ""}`}>
<div className={`${!isEditable ? "opacity-60" : ""}`}>
<div className="flex flex-col gap-3">
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-13 text-tertiary">
<div className="nodedc-external-panel flex min-h-12 items-center gap-3 px-4 py-3">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1.5 text-13 text-tertiary">
<PriorityPropertyIcon className="h-4 w-4 flex-shrink-0" />
<span>{t("priority")}</span>
</div>
@@ -44,14 +44,14 @@ export const ExternalContoursIssueContentProperties = observer(function External
onChange={(val) => issue?.id && issueOperations.update(workspaceSlug, targetProjectId, issue.id, { priority: val })}
disabled={!isEditable}
buttonVariant="border-with-text"
className="w-3/5 flex-grow rounded-sm px-2 hover:bg-layer-1"
className="w-3/5 flex-grow rounded-full px-3 hover:bg-white/6"
buttonContainerClassName="w-full text-left"
buttonClassName="h-auto w-min whitespace-nowrap"
/>
</div>
<div className="flex min-h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-13 text-tertiary">
<div className="nodedc-external-panel flex min-h-12 items-center gap-3 px-4 py-3">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1.5 text-13 text-tertiary">
<LabelPropertyIcon className="h-4 w-4 flex-shrink-0" />
<span>{t("labels")}</span>
</div>
@@ -9,7 +9,6 @@ import { useEffect, useMemo, useRef } from "react";
import { observer } from "mobx-react";
import type { EditorRefApi } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { Badge } from "@plane/propel/badge";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TExternalContourRequest, TIssue, TNameDescriptionLoader } from "@plane/types";
import { EFileAssetType } from "@plane/types";
@@ -139,8 +138,8 @@ export const ExternalContoursIssueMainContent = observer(function ExternalContou
if (!hasDirectTargetAccess) {
return (
<>
<div className="space-y-4 pb-4">
<div className="space-y-4">
<div className="nodedc-external-section space-y-4 p-5">
{isSourceEditable ? (
<>
<IssueTitleInput
@@ -190,23 +189,23 @@ export const ExternalContoursIssueMainContent = observer(function ExternalContou
)}
</div>
<div className="py-4">
<ExternalContoursRequestTraceability contourRequest={contourRequest} />
</div>
<ExternalContoursRequestTraceability contourRequest={contourRequest} />
<div className="py-4">
<div className="mb-2 text-body-sm-medium">{t("external_contours_page.properties.section_title")}</div>
<div className="nodedc-external-section flex flex-col gap-3 px-4 py-4">
<div className="text-body-sm-medium">{t("external_contours_page.properties.section_title")}</div>
<div className="flex flex-col gap-3 text-13 text-secondary">
<div className="flex flex-wrap items-center gap-2">
<div className="nodedc-external-panel flex flex-wrap items-center gap-2 px-4 py-3">
<span className="text-tertiary">{t("priority")}</span>
<Badge variant="neutral">{issue.priority || t("none")}</Badge>
<span className="rounded-full bg-white/6 px-3 py-1.5 text-12 font-medium text-primary">
{issue.priority || t("none")}
</span>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="nodedc-external-panel flex flex-wrap items-center gap-2 px-4 py-3">
<span className="text-tertiary">{t("labels")}</span>
{issue.label_details?.length ? (
issue.label_details.map((label) => (
<div key={label.id} className="flex items-center gap-1 rounded-sm border border-strong px-2 py-1 text-11">
<div key={label.id} className="flex items-center gap-1 rounded-full bg-white/6 px-3 py-1.5 text-11">
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: label.color }} />
<span>{label.name}</span>
</div>
@@ -230,8 +229,12 @@ export const ExternalContoursIssueMainContent = observer(function ExternalContou
<ExternalContoursMirroredActivity activity={mirroredActivity} />
{!isSourceEditable && <div className="py-4 text-13 text-secondary">{t("external_contours_page.readonly_source_view")}</div>}
</>
{!isSourceEditable && (
<div className="nodedc-external-empty px-4 py-4 text-13 text-secondary">
{t("external_contours_page.readonly_source_view")}
</div>
)}
</div>
);
}
@@ -53,14 +53,15 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
onClick={(e) => handleIssueRedirection(e, request.id)}
>
<Row
data-active={selectedInboxIssueId === request.id}
className={cn(
"relative flex cursor-pointer flex-col gap-2 border border-t-transparent border-r-transparent border-b-subtle-1 border-l-transparent py-4 transition-all hover:bg-accent-primary/5",
{ "border border-accent-strong": selectedInboxIssueId === request.id }
"nodedc-external-card relative flex cursor-pointer flex-col gap-4 px-4 py-4 transition-all hover:bg-white/5",
{ "ring-1 ring-accent-primary/35": selectedInboxIssueId === request.id }
)}
>
<div className="space-y-1">
<div className="space-y-2">
<div className="relative flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-11 font-medium text-tertiary">
<div className="flex min-w-0 items-center gap-2 text-11 font-medium text-tertiary">
<span>
{issue.project_detail?.identifier || "REQ"}-{issue.sequence_id}
</span>
@@ -71,9 +72,11 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
</Tooltip>
)}
</div>
<ExternalContourStatePill request={request} />
<div className="shrink-0">
<ExternalContourStatePill request={request} />
</div>
</div>
<h3 className="w-full truncate text-13">{issue.name}</h3>
<h3 className="w-full text-15 leading-6 font-semibold text-primary">{issue.name}</h3>
</div>
<div className="flex items-center justify-between gap-2">
@@ -93,7 +96,7 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
)}
{visibleLabels.map((label) => (
<div key={label.id} className="relative flex !h-[17.5px] items-center gap-1 rounded-sm border border-strong px-1 text-11">
<div key={label.id} className="relative flex h-6 items-center gap-1 rounded-full bg-white/6 px-2 text-11">
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: label.color }} />
<span className="max-w-28 truncate normal-case">{label.name}</span>
</div>
@@ -107,7 +110,7 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
key={assignee.id}
src={assignee.avatar_url || ""}
name={assignee.display_name || "NODE.DC"}
size="md"
size="lg"
showTooltip
/>
))}
@@ -65,20 +65,20 @@ export const ExternalContoursMirroredActivity = observer(function ExternalContou
};
return (
<div className="space-y-3 py-4">
<div className="nodedc-external-section space-y-3 px-4 py-4">
<div className="text-body-sm-medium">{t("external_contours_page.mirror.activity_title")}</div>
{activity.length > 0 ? (
<div className="space-y-3">
{activity.map((item) => (
<div key={item.id} className="rounded-md border border-subtle bg-surface-1 px-4 py-3">
<div key={item.id} className="nodedc-external-panel px-4 py-3">
<div className="text-13 text-primary">{renderMessage(item)}</div>
<div className="mt-1 text-11 text-secondary">{renderFormattedDate(item.created_at)}</div>
</div>
))}
</div>
) : (
<div className="rounded-md border border-dashed border-subtle px-4 py-6 text-13 text-secondary">
<div className="nodedc-external-empty px-4 py-6 text-13 text-secondary">
{t("external_contours_page.mirror.activity_empty")}
</div>
)}
@@ -19,7 +19,7 @@ export const ExternalContoursMirroredAttachments = observer(function ExternalCon
const { t } = useTranslation();
return (
<div className="space-y-3 py-4">
<div className="nodedc-external-section space-y-3 px-4 py-4">
<div className="text-body-sm-medium">{t("external_contours_page.mirror.attachments_title")}</div>
{attachments.length > 0 ? (
@@ -36,7 +36,7 @@ export const ExternalContoursMirroredAttachments = observer(function ExternalCon
href={attachment.download_url || "#"}
target="_blank"
rel="noreferrer"
className="flex min-h-[60px] items-center justify-between gap-3 rounded-md border-[2px] border-subtle bg-surface-1 px-4 py-2 text-13 transition-colors hover:bg-surface-2"
className="nodedc-external-panel flex min-h-[72px] items-center justify-between gap-3 px-4 py-3 text-13 transition-colors hover:bg-white/6"
>
<div className="flex items-center gap-3 overflow-hidden">
<div className="h-7 w-7 flex-shrink-0">{fileIcon}</div>
@@ -55,7 +55,7 @@ export const ExternalContoursMirroredAttachments = observer(function ExternalCon
})}
</div>
) : (
<div className="rounded-md border border-dashed border-subtle px-4 py-6 text-13 text-secondary">
<div className="nodedc-external-empty px-4 py-6 text-13 text-secondary">
{t("external_contours_page.mirror.attachments_empty")}
</div>
)}
@@ -19,7 +19,7 @@ export const ExternalContoursMirroredComments = observer(function ExternalContou
const { t } = useTranslation();
return (
<div className="space-y-3 py-4">
<div className="nodedc-external-section space-y-3 px-4 py-4">
<div className="text-body-sm-medium">{t("external_contours_page.mirror.comments_title")}</div>
{comments.length > 0 ? (
@@ -27,7 +27,7 @@ export const ExternalContoursMirroredComments = observer(function ExternalContou
{comments.map((comment) => {
const actorName = comment.actor_detail?.display_name || t("external_contours_page.mirror.system_actor");
return (
<div key={comment.id} className="rounded-md border border-subtle bg-surface-1 p-4">
<div key={comment.id} className="nodedc-external-panel p-4">
<div className="mb-3 flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Avatar src={comment.actor_detail?.avatar_url || ""} name={actorName} size="md" />
@@ -49,7 +49,7 @@ export const ExternalContoursMirroredComments = observer(function ExternalContou
})}
</div>
) : (
<div className="rounded-md border border-dashed border-subtle px-4 py-6 text-13 text-secondary">
<div className="nodedc-external-empty px-4 py-6 text-13 text-secondary">
{t("external_contours_page.mirror.comments_empty")}
</div>
)}
@@ -17,9 +17,9 @@ type Props = {
};
const TraceabilityCell = ({ label, children }: { label: string; children: ReactNode }) => (
<div className="rounded-md border border-subtle bg-surface-1 p-3">
<div className="nodedc-external-panel p-4">
<div className="text-11 font-medium text-tertiary">{label}</div>
<div className="mt-1 min-h-6 text-13 font-medium text-secondary">{children}</div>
<div className="mt-2 min-h-6 text-13 font-medium text-secondary">{children}</div>
</div>
);
@@ -41,7 +41,7 @@ export const ExternalContoursRequestTraceability = observer(function ExternalCon
const assigneeDetails = issue.assignee_details ?? [];
return (
<div className="rounded-lg border border-subtle bg-surface-2 p-4">
<div className="nodedc-external-section p-5">
<div className="mb-3">
<h5 className="text-body-sm-medium">{t("external_contours_page.traceability.title")}</h5>
<p className="mt-1 text-12 text-tertiary">{t("external_contours_page.traceability.description")}</p>
@@ -73,7 +73,7 @@ export const ExternalContoursRequestTraceability = observer(function ExternalCon
{assigneeDetails.map((assignee) => (
<div key={assignee.id} className="flex items-center gap-2">
<Avatar src={assignee.avatar_url || ""} name={assignee.display_name || t("common.none")} size="sm" showTooltip />
<span className="text-12 font-medium text-secondary">{assignee.display_name}</span>
<span className="text-13 font-medium text-secondary">{assignee.display_name}</span>
</div>
))}
</div>
@@ -90,13 +90,13 @@ export const ExternalContoursRequestTraceability = observer(function ExternalCon
{requestedAt ? renderFormattedDate(requestedAt) : t("common.none")}
</TraceabilityCell>
<TraceabilityCell label={t("external_contours_page.traceability.last_updated")}>
{lastUpdatedAt ? renderFormattedDate(lastUpdatedAt) : t("common.none")}
</TraceabilityCell>
<TraceabilityCell label={t("external_contours_page.traceability.due_date")}>
{dueDate}
</TraceabilityCell>
<TraceabilityCell label={t("external_contours_page.traceability.last_updated")}>
{lastUpdatedAt ? renderFormattedDate(lastUpdatedAt) : t("common.none")}
</TraceabilityCell>
</div>
</div>
);
@@ -97,11 +97,15 @@ export const ExternalContoursRoot = observer(function ExternalContoursRoot(props
inboxIssueId={inboxIssueId.toString()}
/>
) : (
<EmptyStateCompact
assetKey="intake"
title={t("external_contours_page.empty_state.detail_title")}
assetClassName="size-20"
/>
<div className="flex h-full w-full items-center justify-center px-8">
<div className="nodedc-external-section max-w-xl px-8 py-10">
<EmptyStateCompact
assetKey="intake"
title={t("external_contours_page.empty_state.detail_title")}
assetClassName="size-20"
/>
</div>
</div>
)}
</div>
</>
@@ -10,7 +10,6 @@ import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import type { TInboxIssueCurrentTab } from "@plane/types";
import { EInboxIssueCurrentTab } from "@plane/types";
import { EHeaderVariant, Header } from "@plane/ui";
import { cn } from "@plane/utils";
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
import { useAppRouter } from "@/hooks/use-app-router";
@@ -42,18 +41,20 @@ export const ExternalContoursSidebar = observer(function ExternalContoursSidebar
}
}, [currentTab, filteredRequestIds, inboxIssueId, projectId, router, workspaceSlug]);
const currentCount = currentTab === EInboxIssueCurrentTab.CLOSED ? closedRequestIds.length : openRequestIds.length;
return (
<div className="h-full w-full flex-shrink-0 border-r border-strong bg-surface-1">
<div className="nodedc-external-sidebar-shell h-full w-full flex-shrink-0 border-r border-strong/40">
<div className="relative flex h-full w-full flex-col overflow-hidden">
<Header variant={EHeaderVariant.SECONDARY}>
{tabNavigationOptions.map((option) => (
<div
<div className="px-4 py-4">
<div className="nodedc-filter-row-shell flex items-center gap-2 p-1">
{tabNavigationOptions.map((option) => {
const count = option.key === EInboxIssueCurrentTab.CLOSED ? closedRequestIds.length : openRequestIds.length;
return (
<button
type="button"
key={option.key}
data-active={currentTab === option.key}
className={cn(
"relative flex h-full cursor-pointer items-center gap-1 px-3 text-13 font-medium transition-all",
currentTab === option.key ? "text-accent-primary" : "hover:text-secondary"
"nodedc-external-tab flex flex-1 items-center justify-center gap-2 text-13 font-medium transition-all"
)}
onClick={() => {
if (currentTab !== option.key) {
@@ -62,33 +63,34 @@ export const ExternalContoursSidebar = observer(function ExternalContoursSidebar
}
}}
>
<div>{t(option.i18n_label)}</div>
{currentTab === option.key && (
<div className="rounded-full bg-accent-primary/20 px-1.5 py-0.5 text-11 font-semibold text-accent-primary">
{currentCount}
<div>{t(option.i18n_label)}</div>
<div
className={cn(
"rounded-full px-1.5 py-0.5 text-11 font-semibold",
currentTab === option.key ? "bg-accent-primary/15 text-accent-primary" : "bg-white/5 text-secondary"
)}
>
{count}
</div>
)}
<div
className={cn(
"absolute right-0 bottom-0 left-0 rounded-t-md border",
currentTab === option.key ? "border-accent-strong" : "border-transparent"
)}
/>
</div>
))}
</Header>
</button>
);
})}
</div>
</div>
<div className="vertical-scrollbar scrollbar-md h-full w-full overflow-hidden overflow-y-auto">
<div className="vertical-scrollbar scrollbar-md h-full w-full overflow-hidden overflow-y-auto px-4 pb-4">
{filteredRequestIds.length > 0 ? (
filteredRequestIds.map((requestId) => (
<ExternalContoursListItem
key={requestId}
setIsMobileSidebar={setIsMobileSidebar}
workspaceSlug={workspaceSlug}
projectId={projectId}
requestId={requestId}
/>
))
<div className="space-y-3">
{filteredRequestIds.map((requestId) => (
<ExternalContoursListItem
key={requestId}
setIsMobileSidebar={setIsMobileSidebar}
workspaceSlug={workspaceSlug}
projectId={projectId}
requestId={requestId}
/>
))}
</div>
) : (
<div className="flex h-full w-full items-center justify-center">
{currentTab === EInboxIssueCurrentTab.OPEN ? (
@@ -47,7 +47,7 @@ export const ExternalContoursSourceReplyBox = observer(function ExternalContours
};
return (
<div className="space-y-3 py-4">
<div className="nodedc-external-section space-y-3 px-4 py-4">
<div className="text-body-sm-medium">{t("external_contours_page.reply.title")}</div>
<TextArea
value={comment}
@@ -55,9 +55,15 @@ export const ExternalContoursSourceReplyBox = observer(function ExternalContours
rows={4}
placeholder={t("external_contours_page.reply.placeholder")}
disabled={isSubmitting}
className="nodedc-modal-input min-h-[7rem] resize-none !rounded-[1.2rem] !border-0 !bg-white/4"
/>
<div className="flex justify-end">
<Button variant="primary" onClick={handleSubmit} disabled={isSubmitting || !comment.trim()}>
<Button
variant="primary"
onClick={handleSubmit}
disabled={isSubmitting || !comment.trim()}
className="nodedc-modal-primary-button"
>
{t("external_contours_page.reply.submit")}
</Button>
</div>