ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: отправка во внешний контур и source-side список

This commit is contained in:
DCCONSTRUCTIONS
2026-04-18 21:47:29 +03:00
parent 390bcdbf38
commit fb33f093de
28 changed files with 911 additions and 386 deletions
@@ -10,7 +10,7 @@ import useSWR from "swr";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import type { TNameDescriptionLoader } from "@plane/types";
import { ContentWrapper } from "@plane/ui";
import { useProjectInbox } from "@/hooks/store/use-project-inbox";
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
import { useUser, useUserPermissions } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
import { ExternalContoursIssueActionsHeader } from "./issue-header";
@@ -29,11 +29,13 @@ export const ExternalContoursContentRoot = observer(function ExternalContoursCon
const router = useAppRouter();
const [isSubmitting, setIsSubmitting] = useState<TNameDescriptionLoader>("saved");
const { data: currentUser } = useUser();
const { currentTab, fetchInboxIssueById, getIssueInboxByIssueId, getIsIssueAvailable } = useProjectInbox();
const inboxIssue = getIssueInboxByIssueId(inboxIssueId);
const { currentTab, fetchRequestById, getRequestById, getIsRequestAvailable } = useProjectExternalContours();
const contourRequest = getRequestById(inboxIssueId);
const issue = contourRequest?.issue;
const targetProjectId = issue?.project_id || projectId;
const { allowPermissions, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || "");
const isIssueAvailable = getIsRequestAvailable(inboxIssueId?.toString() || "");
useEffect(() => {
if (!isIssueAvailable && inboxIssueId) {
@@ -43,20 +45,23 @@ export const ExternalContoursContentRoot = observer(function ExternalContoursCon
}, [isIssueAvailable]);
useSWR(
workspaceSlug && projectId && inboxIssueId ? `PROJECT_EXTERNAL_CONTOUR_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${inboxIssueId}` : null,
workspaceSlug && projectId && inboxIssueId ? () => fetchInboxIssueById(workspaceSlug, projectId, inboxIssueId) : null,
workspaceSlug && projectId && inboxIssueId
? `PROJECT_EXTERNAL_CONTOUR_DETAIL_${workspaceSlug}_${projectId}_${inboxIssueId}`
: null,
workspaceSlug && projectId && inboxIssueId ? () => fetchRequestById(workspaceSlug, projectId, inboxIssueId) : null,
{ revalidateOnFocus: false, revalidateIfStale: false }
);
const isEditable =
allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId) ||
inboxIssue?.issue.created_by === currentUser?.id;
!!targetProjectId &&
(allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, targetProjectId) ||
issue?.created_by === currentUser?.id);
const isGuest = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId) === EUserPermissions.GUEST;
const isOwner = inboxIssue?.issue.created_by === currentUser?.id;
const isGuest = !!targetProjectId && getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, targetProjectId) === EUserPermissions.GUEST;
const isOwner = issue?.created_by === currentUser?.id;
const readOnly = !isOwner && isGuest;
if (!inboxIssue) return <></>;
if (!contourRequest || !issue) return <></>;
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
@@ -65,17 +70,17 @@ export const ExternalContoursContentRoot = observer(function ExternalContoursCon
setIsMobileSidebar={setIsMobileSidebar}
isMobileSidebar={isMobileSidebar}
workspaceSlug={workspaceSlug}
projectId={projectId}
inboxIssue={inboxIssue}
sourceProjectId={projectId}
contourRequest={contourRequest}
isSubmitting={isSubmitting}
/>
</div>
<ContentWrapper className="divide-y-2 divide-subtle-1">
<ExternalContoursIssueMainContent
workspaceSlug={workspaceSlug}
projectId={projectId}
inboxIssue={inboxIssue}
isEditable={isEditable && !readOnly}
sourceProjectId={projectId}
contourRequest={contourRequest}
isEditable={!!isEditable && !readOnly}
isSubmitting={isSubmitting}
setIsSubmitting={setIsSubmitting}
/>
@@ -12,9 +12,12 @@ import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TIssue } from "@plane/types";
import { EInboxIssueCurrentTab } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
import { useProject } from "@/hooks/store/use-project";
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useAppRouter } from "@/hooks/use-app-router";
import { InboxIssueDescription } from "@/components/inbox/modals/create-modal/issue-description";
import { InboxIssueTitle } from "@/components/inbox/modals/create-modal/issue-title";
import { ExternalContoursCreateProperties } from "./create-properties";
@@ -39,9 +42,11 @@ type Props = {
export const ExternalContoursCreateRoot = observer(function ExternalContoursCreateRoot(props: Props) {
const { workspaceSlug, projectId, handleModalClose } = props;
const { t } = useTranslation();
const router = useAppRouter();
const { getWorkspaceBySlug } = useWorkspace();
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id;
const { currentProjectDetails } = useProject();
const { createRequest } = useProjectExternalContours();
const descriptionEditorRef = useRef<EditorRefApi>(null);
const [createMore, setCreateMore] = useState(false);
const [formSubmitting, setFormSubmitting] = useState(false);
@@ -67,15 +72,32 @@ export const ExternalContoursCreateRoot = observer(function ExternalContoursCrea
}
setFormSubmitting(true);
setToast({
type: TOAST_TYPE.INFO,
title: t("external_contours_page.modal.toast_title"),
message: t("external_contours_page.modal.toast_message"),
});
setFormSubmitting(false);
if (!createMore) {
handleModalClose();
try {
const createdRequest = await createRequest(workspaceSlug, projectId, formData);
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("success"),
message: t("external_contours_page.modal.success_message"),
});
if (createMore) {
setFormData(defaultIssueData);
} else {
handleModalClose();
}
if (createdRequest?.id) {
router.push(`/${workspaceSlug}/projects/${projectId}/external-contours?currentTab=${EInboxIssueCurrentTab.OPEN}&inboxIssueId=${createdRequest.id}`);
}
} catch (error: any) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("error"),
message: error?.error || t("external_contours_page.modal.error_message"),
});
} finally {
setFormSubmitting(false);
}
};
@@ -15,7 +15,7 @@ import { TransferIcon } from "@plane/propel/icons";
import { Breadcrumbs, Header } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { useProject } from "@/hooks/store/use-project";
import { useProjectInbox } from "@/hooks/store/use-project-inbox";
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
import { useUserPermissions } from "@/hooks/store/user";
import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common";
import { ExternalContourCreateModalRoot } from "./create-modal";
@@ -25,8 +25,8 @@ export const ProjectExternalContoursHeader = observer(function ProjectExternalCo
const { workspaceSlug, projectId } = useParams();
const { t } = useTranslation();
const { allowPermissions } = useUserPermissions();
const { currentProjectDetails, loader: currentProjectDetailsLoader } = useProject();
const { loader } = useProjectInbox();
const { loader: currentProjectDetailsLoader } = useProject();
const { loader } = useProjectExternalContours();
const isAuthorized = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
@@ -52,7 +52,7 @@ export const ProjectExternalContoursHeader = observer(function ProjectExternalCo
/>
</Breadcrumbs>
{loader === "pagination-loading" && (
{(loader === "mutation-loading" || loader === "issue-loading") && (
<div className="flex items-center gap-1.5 text-tertiary">
<RefreshCcw className="h-3.5 w-3.5 animate-spin" />
<p className="text-13">{t("syncing")}...</p>
@@ -61,7 +61,7 @@ export const ProjectExternalContoursHeader = observer(function ProjectExternalCo
</div>
</Header.LeftItem>
<Header.RightItem>
{currentProjectDetails?.inbox_view && workspaceSlug && projectId && isAuthorized ? (
{workspaceSlug && projectId && isAuthorized ? (
<div className="flex items-center gap-2">
<ExternalContourCreateModalRoot
workspaceSlug={workspaceSlug.toString()}
@@ -10,51 +10,49 @@ import { PanelLeft } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { IconButton } from "@plane/propel/icon-button";
import { CheckCircleFilledIcon, ChevronDownIcon, ChevronUpIcon, CloseCircleFilledIcon, LinkIcon, NewTabIcon } from "@plane/propel/icons";
import { ChevronDownIcon, ChevronUpIcon, LinkIcon, NewTabIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TNameDescriptionLoader } from "@plane/types";
import { EInboxIssueCurrentTab } from "@plane/types";
import type { TExternalContourRequest, TNameDescriptionLoader } from "@plane/types";
import { ControlLink, Header, Row } from "@plane/ui";
import { copyUrlToClipboard, generateWorkItemLink } from "@plane/utils";
import { NameDescriptionUpdateStatus } from "@/components/issues/issue-update-status";
import { useProject } from "@/hooks/store/use-project";
import { useProjectInbox } from "@/hooks/store/use-project-inbox";
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
import { useAppRouter } from "@/hooks/use-app-router";
import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
import { InboxIssueStatus } from "@/components/inbox/inbox-issue-status";
import { ExternalContourStatePill } from "./state-pill";
type Props = {
workspaceSlug: string;
projectId: string;
inboxIssue: IInboxIssueStore | undefined;
sourceProjectId: string;
contourRequest: TExternalContourRequest;
isSubmitting: TNameDescriptionLoader;
isMobileSidebar: boolean;
setIsMobileSidebar: (value: boolean) => void;
};
export const ExternalContoursIssueActionsHeader = observer(function ExternalContoursIssueActionsHeader(props: Props) {
const { workspaceSlug, projectId, inboxIssue, isSubmitting, isMobileSidebar, setIsMobileSidebar } = props;
const { workspaceSlug, sourceProjectId, contourRequest, isSubmitting, isMobileSidebar, setIsMobileSidebar } = props;
const { t } = useTranslation();
const router = useAppRouter();
const { currentTab, filteredInboxIssueIds } = useProjectInbox();
const { currentTab, filteredRequestIds } = useProjectExternalContours();
const { getProjectById } = useProject();
const issue = inboxIssue?.issue;
const currentInboxIssueId = issue?.id;
const issue = contourRequest.issue;
const currentRequestId = contourRequest.id;
const redirectToRelativeIssue = useCallback(
(direction: "next" | "prev") => {
if (!filteredInboxIssueIds || !currentInboxIssueId) return;
const currentIssueIndex = filteredInboxIssueIds.findIndex((issueId) => issueId === currentInboxIssueId);
if (!filteredRequestIds || !currentRequestId) return;
const currentIssueIndex = filteredRequestIds.findIndex((requestId) => requestId === currentRequestId);
const nextIssueIndex =
direction === "next"
? (currentIssueIndex + 1) % filteredInboxIssueIds.length
: (currentIssueIndex - 1 + filteredInboxIssueIds.length) % filteredInboxIssueIds.length;
const nextIssueId = filteredInboxIssueIds[nextIssueIndex];
? (currentIssueIndex + 1) % filteredRequestIds.length
: (currentIssueIndex - 1 + filteredRequestIds.length) % filteredRequestIds.length;
const nextIssueId = filteredRequestIds[nextIssueIndex];
if (!nextIssueId) return;
router.push(`/${workspaceSlug}/projects/${projectId}/external-contours?currentTab=${currentTab}&inboxIssueId=${nextIssueId}`);
router.push(`/${workspaceSlug}/projects/${sourceProjectId}/external-contours?currentTab=${currentTab}&inboxIssueId=${nextIssueId}`);
},
[currentInboxIssueId, currentTab, filteredInboxIssueIds, projectId, router, workspaceSlug]
[currentRequestId, currentTab, filteredRequestIds, router, sourceProjectId, workspaceSlug]
);
useEffect(() => {
@@ -67,23 +65,15 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont
return () => document.removeEventListener("keydown", onKeyDown);
}, [redirectToRelativeIssue]);
if (!issue || !inboxIssue) return null;
const targetProjectIdentifier = issue.project_detail?.identifier || getProjectById(issue.project_id || "")?.identifier;
const workItemLink = generateWorkItemLink({
workspaceSlug: workspaceSlug?.toString(),
projectId: issue.project_id,
issueId: currentInboxIssueId,
projectIdentifier: getProjectById(issue.project_id)?.identifier,
issueId: issue.id,
projectIdentifier: targetProjectIdentifier,
sequenceId: issue.sequence_id,
});
const showWorkflowToast = (actionLabel: string) =>
setToast({
type: TOAST_TYPE.INFO,
title: t("external_contours_page.actions.unsupported_title"),
message: t("external_contours_page.actions.unsupported_message", { action: actionLabel.toLowerCase() }),
});
const handleCopyLink = () =>
copyUrlToClipboard(workItemLink).then(() =>
setToast({
@@ -93,18 +83,16 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont
})
);
const isOpenTab = currentTab === EInboxIssueCurrentTab.OPEN;
return (
<>
<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 && (
<h3 className="flex-shrink-0 text-14 font-medium text-tertiary">
{getProjectById(issue.project_id)?.identifier}-{issue.sequence_id}
{targetProjectIdentifier}-{issue.sequence_id}
</h3>
)}
<InboxIssueStatus inboxIssue={inboxIssue} iconSize={12} />
<ExternalContourStatePill request={contourRequest} />
<div className="flex w-full items-center justify-end">
<NameDescriptionUpdateStatus isSubmitting={isSubmitting} />
</div>
@@ -117,30 +105,6 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont
</div>
<div className="flex flex-wrap items-center gap-2">
{isOpenTab ? (
<>
<Button variant="secondary" size="lg" onClick={() => showWorkflowToast(t("external_contours_page.actions.send"))}>
<CheckCircleFilledIcon className="size-4 shrink-0 text-success-secondary" />
{t("external_contours_page.actions.send")}
</Button>
<Button variant="secondary" size="lg" onClick={() => showWorkflowToast(t("external_contours_page.actions.decline"))}>
<CloseCircleFilledIcon className="size-4 shrink-0 text-danger-secondary" />
{t("external_contours_page.actions.decline")}
</Button>
</>
) : (
<>
<Button variant="secondary" size="lg" onClick={() => showWorkflowToast(t("external_contours_page.actions.accept"))}>
<CheckCircleFilledIcon className="size-4 shrink-0 text-success-secondary" />
{t("external_contours_page.actions.accept")}
</Button>
<Button variant="secondary" size="lg" onClick={() => showWorkflowToast(t("external_contours_page.actions.decline"))}>
<CloseCircleFilledIcon className="size-4 shrink-0 text-danger-secondary" />
{t("external_contours_page.actions.decline")}
</Button>
</>
)}
<Button variant="secondary" size="lg" prependIcon={<LinkIcon className="h-2.5 w-2.5" />} onClick={handleCopyLink}>
{t("external_contours_page.actions.copy")}
</Button>
@@ -159,14 +123,13 @@ export const ExternalContoursIssueActionsHeader = observer(function ExternalCont
className={`my-auto mr-2 h-4 w-4 flex-shrink-0 ${isMobileSidebar ? "text-accent-primary" : "text-secondary"}`}
/>
<div className="flex w-full items-center gap-2">
<InboxIssueStatus inboxIssue={inboxIssue} iconSize={12} />
<ExternalContourStatePill request={contourRequest} />
<div className="ml-auto flex items-center gap-2">
<Button variant="secondary" size="sm" onClick={() => showWorkflowToast(isOpenTab ? t("external_contours_page.actions.send") : t("external_contours_page.actions.accept"))}>
{isOpenTab ? t("external_contours_page.actions.send") : t("external_contours_page.actions.accept")}
</Button>
<Button variant="secondary" size="sm" onClick={() => showWorkflowToast(t("external_contours_page.actions.decline"))}>
{t("external_contours_page.actions.decline")}
</Button>
<ControlLink href={workItemLink} onClick={() => router.push(workItemLink)} target="_self">
<Button variant="secondary" size="sm">
{t("external_contours_page.actions.open")}
</Button>
</ControlLink>
</div>
</div>
</Header>
@@ -7,52 +7,39 @@
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import {
DuplicatePropertyIcon,
DueDatePropertyIcon,
LabelPropertyIcon,
MembersPropertyIcon,
PriorityPropertyIcon,
StatePropertyIcon,
} from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import type { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
import { ControlLink } from "@plane/ui";
import { generateWorkItemLink, getDate, renderFormattedPayloadDate } from "@plane/utils";
import type { TIssue } from "@plane/types";
import { Badge } from "@plane/propel/badge";
import { getDate, renderFormattedPayloadDate } from "@plane/utils";
import { DateDropdown } from "@/components/dropdowns/date";
import { MemberDropdown } from "@/components/dropdowns/member/dropdown";
import { PriorityDropdown } from "@/components/dropdowns/priority";
import { IssueLabelSelect } from "@/components/issues/select";
import type { TIssueOperations } from "@/components/issues/issue-detail";
import { IssueLabel } from "@/components/issues/issue-detail/label";
import { useProject } from "@/hooks/store/use-project";
import { useAppRouter } from "@/hooks/use-app-router";
type Props = {
workspaceSlug: string;
projectId: string;
issue: Partial<TIssue>;
targetProjectId: string;
issue: Partial<TIssue> & {
project_detail?: { name?: string } | null;
};
issueOperations: TIssueOperations;
isEditable: boolean;
duplicateIssueDetails: TInboxDuplicateIssueDetails | undefined;
};
export const ExternalContoursIssueContentProperties = observer(function ExternalContoursIssueContentProperties(props: Props) {
const { workspaceSlug, projectId, issue, issueOperations, isEditable, duplicateIssueDetails } = props;
const { workspaceSlug, targetProjectId, issue, issueOperations, isEditable } = props;
const { t } = useTranslation();
const router = useAppRouter();
const { currentProjectDetails } = useProject();
const minDate = issue.start_date ? getDate(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
if (!issue || !issue?.id) return <></>;
const duplicateWorkItemLink = generateWorkItemLink({
workspaceSlug: workspaceSlug?.toString(),
projectId,
issueId: duplicateIssueDetails?.id,
projectIdentifier: currentProjectDetails?.identifier,
sequenceId: duplicateIssueDetails?.sequence_id,
});
return (
<div className="flex w-full flex-col divide-y-2 divide-subtle-1">
<div className="w-full overflow-y-auto">
@@ -64,8 +51,8 @@ export const ExternalContoursIssueContentProperties = observer(function External
<StatePropertyIcon className="h-4 w-4 flex-shrink-0" />
<span>{t("external_contours_page.properties.target_contour")}</span>
</div>
<div className="w-3/5 flex-grow text-13 text-placeholder">
{t("external_contours_page.properties.target_contour_placeholder")}
<div className="w-3/5 flex-grow text-13">
<Badge variant="neutral">{issue.project_detail?.name || t("common.none")}</Badge>
</div>
</div>
@@ -76,9 +63,9 @@ export const ExternalContoursIssueContentProperties = observer(function External
</div>
<MemberDropdown
value={issue?.assignee_ids ?? []}
onChange={(val) => issue?.id && issueOperations.update(workspaceSlug, projectId, issue.id, { assignee_ids: val })}
onChange={(val) => issue?.id && issueOperations.update(workspaceSlug, targetProjectId, issue.id, { assignee_ids: val })}
disabled={!isEditable}
projectId={projectId?.toString() ?? ""}
projectId={targetProjectId}
placeholder={t("assignee")}
multiple
buttonVariant={(issue?.assignee_ids || []).length > 0 ? "transparent-without-text" : "transparent-with-text"}
@@ -98,7 +85,7 @@ export const ExternalContoursIssueContentProperties = observer(function External
</div>
<PriorityDropdown
value={issue?.priority}
onChange={(val) => issue?.id && issueOperations.update(workspaceSlug, projectId, issue.id, { priority: val })}
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"
@@ -121,7 +108,7 @@ export const ExternalContoursIssueContentProperties = observer(function External
value={issue.target_date || null}
onChange={(val) =>
issue?.id &&
issueOperations.update(workspaceSlug, projectId, issue.id, {
issueOperations.update(workspaceSlug, targetProjectId, issue.id, {
target_date: val ? renderFormattedPayloadDate(val) : null,
})
}
@@ -142,34 +129,14 @@ export const ExternalContoursIssueContentProperties = observer(function External
<span>{t("labels")}</span>
</div>
<div className="h-full min-h-8 w-3/5 flex-grow pt-1">
{issue?.id && (
<IssueLabel
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issue.id}
disabled={!isEditable}
isInboxIssue
onLabelUpdate={(val: string[]) => issue?.id && issueOperations.update(workspaceSlug, projectId, issue.id, { label_ids: val })}
/>
)}
<IssueLabelSelect
value={issue.label_ids || []}
onChange={(labelIds) => issue?.id && issueOperations.update(workspaceSlug, targetProjectId, issue.id, { label_ids: labelIds })}
projectId={targetProjectId}
disabled={!isEditable}
/>
</div>
</div>
{duplicateIssueDetails && (
<div className="flex min-h-8 gap-2">
<div className="flex w-2/5 flex-shrink-0 gap-1 pt-2 text-13 text-tertiary">
<DuplicatePropertyIcon className="h-4 w-4 flex-shrink-0" />
<span>{t("external_contours_page.properties.duplicate_of")}</span>
</div>
<ControlLink href={duplicateWorkItemLink} onClick={() => router.push(duplicateWorkItemLink)} target="_self">
<Tooltip tooltipContent={`${duplicateIssueDetails?.name}`}>
<span className="flex cursor-pointer items-center gap-1 rounded-sm bg-layer-1 px-1.5 py-1 pb-0.5 text-11 text-secondary">
{`${currentProjectDetails?.identifier}-${duplicateIssueDetails?.sequence_id}`}
</span>
</Tooltip>
</ControlLink>
</div>
)}
</div>
</div>
</div>
@@ -10,8 +10,8 @@ import { observer } from "mobx-react";
import type { EditorRefApi } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TIssue, TNameDescriptionLoader } from "@plane/types";
import { EFileAssetType, EInboxIssueSource } from "@plane/types";
import type { TExternalContourRequest, TIssue, TNameDescriptionLoader } from "@plane/types";
import { EFileAssetType } from "@plane/types";
import { getTextContent } from "@plane/utils";
import { DescriptionVersionsRoot } from "@/components/core/description-versions";
import { DescriptionInput } from "@/components/editor/rich-text/description-input";
@@ -21,38 +21,33 @@ import type { TIssueOperations } from "@/components/issues/issue-detail";
import { IssueActivity } from "@/components/issues/issue-detail/issue-activity";
import { IssueReaction } from "@/components/issues/issue-detail/reactions";
import { IssueTitleInput } from "@/components/issues/title-input";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useMember } from "@/hooks/store/use-member";
import { useProject } from "@/hooks/store/use-project";
import { useProjectInbox } from "@/hooks/store/use-project-inbox";
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
import { useUser } from "@/hooks/store/user";
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
import { DeDupeIssuePopoverRoot } from "@/plane-web/components/de-dupe/duplicate-popover";
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
import { IntakeWorkItemVersionService } from "@/services/inbox";
import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
import { IssueService } from "@/services/issue/issue.service";
import { WorkItemVersionService } from "@/services/issue/work_item_version.service";
import { ExternalContoursIssueContentProperties } from "./issue-properties";
const intakeWorkItemVersionService = new IntakeWorkItemVersionService();
const workItemVersionService = new WorkItemVersionService();
const issueService = new IssueService();
type Props = {
workspaceSlug: string;
projectId: string;
inboxIssue: IInboxIssueStore;
sourceProjectId: string;
contourRequest: TExternalContourRequest;
isEditable: boolean;
isSubmitting: TNameDescriptionLoader;
setIsSubmitting: Dispatch<SetStateAction<TNameDescriptionLoader>>;
};
export const ExternalContoursIssueMainContent = observer(function ExternalContoursIssueMainContent(props: Props) {
const { workspaceSlug, projectId, inboxIssue, isEditable, isSubmitting, setIsSubmitting } = props;
const { workspaceSlug, sourceProjectId, contourRequest, isEditable, isSubmitting, setIsSubmitting } = props;
const { t } = useTranslation();
const editorRef = useRef<EditorRefApi>(null);
const { data: currentUser } = useUser();
const { getUserDetails } = useMember();
const { loader } = useProjectInbox();
const { getProjectById } = useProject();
const { removeIssue, archiveIssue } = useIssueDetail();
const { loader, updateRequestIssue } = useProjectExternalContours();
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
useEffect(() => {
@@ -64,43 +59,42 @@ export const ExternalContoursIssueMainContent = observer(function ExternalContou
}
}, [isSubmitting, setIsSubmitting, setShowAlert]);
const issue = inboxIssue.issue;
const projectDetails = issue?.project_id ? getProjectById(issue.project_id) : undefined;
const issue = contourRequest.issue;
const targetProjectId = issue.project_id || sourceProjectId;
const { duplicateIssues } = useDebouncedDuplicateIssues(workspaceSlug, projectDetails?.workspace.toString(), projectId, {
name: issue?.name,
description_html: getTextContent(issue?.description_html),
issueId: issue?.id,
});
const { duplicateIssues } = useDebouncedDuplicateIssues(
workspaceSlug,
targetProjectId,
targetProjectId,
{
name: issue?.name,
description_html: getTextContent(issue?.description_html),
issueId: issue?.id,
}
);
const issueOperations: TIssueOperations = useMemo(
() => ({
fetch: async () => undefined,
remove: async (_workspaceSlug: string, _projectId: string, issueId: string) => {
try {
await removeIssue(workspaceSlug, projectId, issueId);
await issueService.deleteIssue(workspaceSlug, targetProjectId, issueId);
setToast({ title: t("success"), type: TOAST_TYPE.SUCCESS, message: t("inbox_issue.modals.delete.success") });
} catch (error) {
console.log("Error in deleting work item:", error);
} catch {
setToast({ title: t("error"), type: TOAST_TYPE.ERROR, message: t("something_went_wrong_please_try_again") });
}
},
update: async (_workspaceSlug: string, _projectId: string, _issueId: string, data: Partial<TIssue>) => {
update: async (_workspaceSlug: string, _projectId: string, issueId: string, data: Partial<TIssue>) => {
try {
await inboxIssue.updateIssue(data);
const updatedIssue = await issueService.patchIssue(workspaceSlug, targetProjectId, issueId, data);
updateRequestIssue(contourRequest.id, { ...data, ...updatedIssue });
} catch {
setToast({ title: t("error"), type: TOAST_TYPE.ERROR, message: t("issue_could_not_be_updated") });
}
},
archive: async (_workspaceSlug: string, _projectId: string, issueId: string) => {
try {
await archiveIssue(workspaceSlug, projectId, issueId);
} catch (error) {
console.error("Error in archiving issue:", error);
}
},
archive: async () => undefined,
}),
[archiveIssue, inboxIssue, projectId, removeIssue, workspaceSlug]
[contourRequest.id, targetProjectId, t, updateRequestIssue, workspaceSlug]
);
if (!issue || !issue.project_id || !issue.id) return <></>;
@@ -111,16 +105,15 @@ export const ExternalContoursIssueMainContent = observer(function ExternalContou
{duplicateIssues.length > 0 && (
<DeDupeIssuePopoverRoot
workspaceSlug={workspaceSlug}
projectId={issue.project_id}
projectId={targetProjectId}
rootIssueId={issue.id}
issues={duplicateIssues}
issueOperations={issueOperations}
isIntakeIssue
/>
)}
<IssueTitleInput
workspaceSlug={workspaceSlug}
projectId={issue.project_id}
projectId={targetProjectId}
issueId={issue.id}
isSubmitting={isSubmitting}
setIsSubmitting={(value) => setIsSubmitting(value)}
@@ -143,12 +136,12 @@ export const ExternalContoursIssueMainContent = observer(function ExternalContou
initialValue={!issue.description_html || issue.description_html === "" ? "<p></p>" : issue.description_html}
key={issue.id}
onSubmit={async (value, isMigrationUpdate) => {
await issueOperations.update(workspaceSlug, issue.project_id, issue.id, {
await issueOperations.update(workspaceSlug, targetProjectId, issue.id, {
description_html: value.description_html,
...(isMigrationUpdate ? { skip_activity: "true" } : {}),
});
}}
projectId={issue.project_id}
projectId={targetProjectId}
setIsSubmitting={(value) => setIsSubmitting(value)}
workspaceSlug={workspaceSlug}
/>
@@ -156,27 +149,25 @@ export const ExternalContoursIssueMainContent = observer(function ExternalContou
<div className="flex items-center justify-between gap-2">
{currentUser && (
<IssueReaction workspaceSlug={workspaceSlug} projectId={projectId} issueId={issue.id} currentUser={currentUser} />
<IssueReaction workspaceSlug={workspaceSlug} projectId={targetProjectId} issueId={issue.id} currentUser={currentUser} />
)}
{isEditable && (
<DescriptionVersionsRoot
className="flex-shrink-0"
entityInformation={{
createdAt: issue.created_at ? new Date(issue.created_at) : new Date(),
createdByDisplayName:
inboxIssue.source === EInboxIssueSource.FORMS
? t("inbox_issue.source.form_user")
: (getUserDetails(issue.created_by ?? "")?.display_name ?? ""),
createdByDisplayName: issue.created_by_detail?.display_name ?? "",
id: issue.id,
isRestoreDisabled: !isEditable,
}}
fetchHandlers={{
listDescriptionVersions: (issueId) => intakeWorkItemVersionService.listDescriptionVersions(workspaceSlug, projectId, issueId),
listDescriptionVersions: (issueId) =>
workItemVersionService.listDescriptionVersions(workspaceSlug, targetProjectId, issueId),
retrieveDescriptionVersion: (issueId, versionId) =>
intakeWorkItemVersionService.retrieveDescriptionVersion(workspaceSlug, projectId, issueId, versionId),
workItemVersionService.retrieveDescriptionVersion(workspaceSlug, targetProjectId, issueId, versionId),
}}
handleRestore={(descriptionHTML) => editorRef.current?.setEditorValue(descriptionHTML, true)}
projectId={projectId}
projectId={targetProjectId}
workspaceSlug={workspaceSlug}
/>
)}
@@ -184,22 +175,21 @@ export const ExternalContoursIssueMainContent = observer(function ExternalContou
</div>
<div className="py-4">
<IssueAttachmentRoot workspaceSlug={workspaceSlug} projectId={projectId} issueId={issue.id} disabled={!isEditable} />
<IssueAttachmentRoot workspaceSlug={workspaceSlug} projectId={targetProjectId} issueId={issue.id} disabled={!isEditable} />
</div>
<div className="py-4">
<ExternalContoursIssueContentProperties
workspaceSlug={workspaceSlug}
projectId={projectId}
targetProjectId={targetProjectId}
issue={issue}
issueOperations={issueOperations}
isEditable={isEditable}
duplicateIssueDetails={inboxIssue.duplicate_issue_detail}
/>
</div>
<div className="pt-4">
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issue.id} isIntakeIssue />
<IssueActivity workspaceSlug={workspaceSlug} projectId={targetProjectId} issueId={issue.id} />
</div>
</>
);
@@ -12,71 +12,65 @@ import { useTranslation } from "@plane/i18n";
import { PriorityIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { Avatar, Row } from "@plane/ui";
import { cn, getFileURL, renderFormattedDate } from "@plane/utils";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { useLabel } from "@/hooks/store/use-label";
import { useMember } from "@/hooks/store/use-member";
import { useProjectInbox } from "@/hooks/store/use-project-inbox";
import { cn, renderFormattedDate } from "@plane/utils";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { InboxSourcePill } from "@/plane-web/components/inbox/source-pill";
import { InboxIssueStatus } from "@/components/inbox/inbox-issue-status";
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
import { ExternalContourStatePill } from "./state-pill";
type Props = {
workspaceSlug: string;
projectId: string;
projectIdentifier?: string;
inboxIssueId: string;
requestId: string;
setIsMobileSidebar: (value: boolean) => void;
};
export const ExternalContoursListItem = observer(function ExternalContoursListItem(props: Props) {
const { workspaceSlug, projectId, inboxIssueId, projectIdentifier, setIsMobileSidebar } = props;
const { workspaceSlug, projectId, requestId, setIsMobileSidebar } = props;
const searchParams = useSearchParams();
const selectedInboxIssueId = searchParams.get("inboxIssueId");
const { t } = useTranslation();
const { currentTab, getIssueInboxByIssueId } = useProjectInbox();
const { projectLabels } = useLabel();
const { currentTab, getRequestById } = useProjectExternalContours();
const { isMobile } = usePlatformOS();
const { getUserDetails } = useMember();
const inboxIssue = getIssueInboxByIssueId(inboxIssueId);
const issue = inboxIssue?.issue;
const request = getRequestById(requestId);
const issue = request?.issue;
const handleIssueRedirection = (event: MouseEvent, currentIssueId: string | undefined) => {
if (selectedInboxIssueId === currentIssueId) event.preventDefault();
setIsMobileSidebar(false);
};
if (!issue) return <></>;
if (!request || !issue) return <></>;
const createdByDetails = issue?.created_by ? getUserDetails(issue?.created_by) : undefined;
const createdByDetails = issue.created_by_detail;
const visibleLabels = issue.label_details?.slice(0, 3) ?? [];
return (
<Link
id={`external-contour-list-item-${issue.id}`}
key={`${projectId}_${issue.id}`}
href={`/${workspaceSlug}/projects/${projectId}/external-contours?currentTab=${currentTab}&inboxIssueId=${issue.id}`}
onClick={(e) => handleIssueRedirection(e, issue.id)}
id={`external-contour-list-item-${request.id}`}
key={`${projectId}_${request.id}`}
href={`/${workspaceSlug}/projects/${projectId}/external-contours?currentTab=${currentTab}&inboxIssueId=${request.id}`}
onClick={(e) => handleIssueRedirection(e, request.id)}
>
<Row
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 === issue.id }
{ "border border-accent-strong": selectedInboxIssueId === request.id }
)}
>
<div className="space-y-1">
<div className="relative flex items-center justify-between gap-2">
<div className="flex-shrink-0 text-11 font-medium text-tertiary">
{projectIdentifier}-{issue.sequence_id}
</div>
<div className="flex items-center gap-2">
{inboxIssue.source && <InboxSourcePill source={inboxIssue.source} />}
{inboxIssue.status !== -2 && <InboxIssueStatus inboxIssue={inboxIssue} iconSize={12} />}
<div className="flex items-center gap-2 text-11 font-medium text-tertiary">
<span>
{issue.project_detail?.identifier || "REQ"}-{issue.sequence_id}
</span>
{issue.project_detail?.name && <span className="truncate text-placeholder">{issue.project_detail.name}</span>}
</div>
<ExternalContourStatePill request={request} />
</div>
<h3 className="w-full truncate text-13">{issue.name}</h3>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<Tooltip
tooltipHeading={t("issues.properties.created_on")}
@@ -86,41 +80,21 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
<div className="text-11 text-secondary">{renderFormattedDate(issue.created_at ?? "")}</div>
</Tooltip>
<div className="rounded-full border-2 border-strong-1" />
{issue.priority && (
{issue.priority && issue.priority !== "none" && (
<Tooltip tooltipHeading={t("priority")} tooltipContent={`${issue.priority ?? t("none")}`}>
<PriorityIcon priority={issue.priority} withContainer className="h-3 w-3" />
</Tooltip>
)}
{issue.label_ids && issue.label_ids.length > 3 ? (
<div className="relative flex !h-[17.5px] items-center gap-1 rounded-sm border border-strong px-1 text-11">
<span className="h-2 w-2 rounded-full bg-orange-400" />
<span className="max-w-28 truncate normal-case">{`${issue.label_ids.length} ${t("labels").toLowerCase()}`}</span>
{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">
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: label.color }} />
<span className="max-w-28 truncate normal-case">{label.name}</span>
</div>
) : (
<>
{(issue.label_ids ?? []).map((labelId) => {
const labelDetails = projectLabels?.find((l) => l.id === labelId);
if (!labelDetails) return null;
return (
<div
key={labelId}
className="relative flex !h-[17.5px] items-center gap-1 rounded-sm border border-strong px-1 text-11"
>
<span className="h-2 w-2 rounded-full" style={{ backgroundColor: labelDetails.color }} />
<span className="max-w-28 truncate normal-case">{labelDetails.name}</span>
</div>
);
})}
</>
)}
))}
</div>
{createdByDetails && createdByDetails.email?.includes("intake@plane.so") ? (
<Avatar src={getFileURL("")} name={"NODE.DC"} size="md" showTooltip />
) : createdByDetails ? (
<ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />
{createdByDetails ? (
<Avatar src={createdByDetails.avatar_url || ""} name={createdByDetails.display_name || "NODE.DC"} size="md" showTooltip />
) : null}
</div>
</Row>
@@ -14,7 +14,7 @@ import type { TInboxIssueCurrentTab } from "@plane/types";
import { EInboxIssueCurrentTab } from "@plane/types";
import { cn } from "@plane/utils";
import { InboxLayoutLoader } from "@/components/ui/loader/layouts/project-inbox/inbox-layout-loader";
import { useProjectInbox } from "@/hooks/store/use-project-inbox";
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
import { ExternalContoursContentRoot } from "./content-root";
import { ExternalContoursSidebar } from "./sidebar";
@@ -22,30 +22,29 @@ type TExternalContoursRoot = {
workspaceSlug: string;
projectId: string;
inboxIssueId: string | undefined;
inboxAccessible: boolean;
navigationTab?: TInboxIssueCurrentTab | undefined;
};
export const ExternalContoursRoot = observer(function ExternalContoursRoot(props: TExternalContoursRoot) {
const { workspaceSlug, projectId, inboxIssueId, inboxAccessible, navigationTab } = props;
const { workspaceSlug, projectId, inboxIssueId, navigationTab } = props;
const [isMobileSidebar, setIsMobileSidebar] = useState(true);
const { t } = useTranslation();
const { loader, error, currentTab, currentInboxProjectId, handleCurrentTab, fetchInboxIssues } = useProjectInbox();
const { loader, error, currentTab, currentProjectId, handleCurrentTab, fetchRequests } = useProjectExternalContours();
useEffect(() => {
if (!inboxAccessible || !workspaceSlug || !projectId) return;
if (!workspaceSlug || !projectId) return;
const hasProjectChanged = currentInboxProjectId && currentInboxProjectId !== projectId;
const hasProjectChanged = currentProjectId && currentProjectId !== projectId;
if (navigationTab && navigationTab !== currentTab) {
handleCurrentTab(workspaceSlug, projectId, navigationTab);
} else if (hasProjectChanged) {
handleCurrentTab(workspaceSlug, projectId, EInboxIssueCurrentTab.OPEN);
} else {
fetchInboxIssues(workspaceSlug.toString(), projectId.toString(), undefined, navigationTab || EInboxIssueCurrentTab.OPEN);
fetchRequests(workspaceSlug.toString(), projectId.toString(), navigationTab || EInboxIssueCurrentTab.OPEN);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inboxAccessible, workspaceSlug, projectId]);
}, [workspaceSlug, projectId]);
if (loader === "init-loading") {
return (
@@ -4,21 +4,16 @@
* See the LICENSE file for details.
*/
import { useCallback, useEffect, useRef, useState } from "react";
import { useEffect } from "react";
import { observer } from "mobx-react";
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, Loader } from "@plane/ui";
import { EHeaderVariant, Header } from "@plane/ui";
import { cn } from "@plane/utils";
import { InboxSidebarLoader } from "@/components/ui/loader/layouts/project-inbox/inbox-sidebar-loader";
import { useProject } from "@/hooks/store/use-project";
import { useProjectInbox } from "@/hooks/store/use-project-inbox";
import { useProjectExternalContours } from "@/hooks/store/use-project-external-contours";
import { useAppRouter } from "@/hooks/use-app-router";
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
import { FiltersRoot } from "@/components/inbox/inbox-filter";
import { InboxIssueAppliedFilters } from "@/components/inbox/inbox-filter/applied-filters/root";
import { ExternalContoursListItem } from "./list-item";
type Props = {
@@ -36,34 +31,18 @@ const tabNavigationOptions: { key: TInboxIssueCurrentTab; i18n_label: string }[]
export const ExternalContoursSidebar = observer(function ExternalContoursSidebar(props: Props) {
const { workspaceSlug, projectId, inboxIssueId, setIsMobileSidebar } = props;
const router = useAppRouter();
const containerRef = useRef<HTMLDivElement>(null);
const [elementRef, setElementRef] = useState<HTMLDivElement | null>(null);
const { t } = useTranslation();
const { currentProjectDetails } = useProject();
const {
currentTab,
handleCurrentTab,
loader,
filteredInboxIssueIds,
inboxIssuePaginationInfo,
fetchInboxPaginationIssues,
getAppliedFiltersCount,
} = useProjectInbox();
const fetchNextPages = useCallback(() => {
if (!workspaceSlug || !projectId) return;
fetchInboxPaginationIssues(workspaceSlug.toString(), projectId.toString());
}, [workspaceSlug, projectId, fetchInboxPaginationIssues]);
useIntersectionObserver(containerRef, elementRef, fetchNextPages, "20%");
const { currentTab, handleCurrentTab, filteredRequestIds, openRequestIds, closedRequestIds } = useProjectExternalContours();
useEffect(() => {
if (workspaceSlug && projectId && currentTab && filteredInboxIssueIds.length > 0 && inboxIssueId === undefined) {
if (workspaceSlug && projectId && filteredRequestIds.length > 0 && inboxIssueId === undefined) {
router.push(
`/${workspaceSlug}/projects/${projectId}/external-contours?currentTab=${currentTab}&inboxIssueId=${filteredInboxIssueIds[0]}`
`/${workspaceSlug}/projects/${projectId}/external-contours?currentTab=${currentTab}&inboxIssueId=${filteredRequestIds[0]}`
);
}
}, [currentTab, filteredInboxIssueIds, inboxIssueId, projectId, router, workspaceSlug]);
}, [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">
@@ -84,9 +63,9 @@ export const ExternalContoursSidebar = observer(function ExternalContoursSidebar
}}
>
<div>{t(option.i18n_label)}</div>
{option.key === EInboxIssueCurrentTab.OPEN && currentTab === option.key && (
{currentTab === option.key && (
<div className="rounded-full bg-accent-primary/20 px-1.5 py-0.5 text-11 font-semibold text-accent-primary">
{inboxIssuePaginationInfo?.total_results || 0}
{currentCount}
</div>
)}
<div
@@ -97,67 +76,41 @@ export const ExternalContoursSidebar = observer(function ExternalContoursSidebar
/>
</div>
))}
<div className="m-auto mr-0">
<FiltersRoot />
</div>
</Header>
<InboxIssueAppliedFilters />
{loader === "filter-loading" && !inboxIssuePaginationInfo?.next_page_results ? (
<InboxSidebarLoader />
) : (
<div className="vertical-scrollbar scrollbar-md h-full w-full overflow-hidden overflow-y-auto" ref={containerRef}>
{filteredInboxIssueIds.length > 0 ? (
filteredInboxIssueIds.map((inboxId) => (
<ExternalContoursListItem
key={inboxId}
setIsMobileSidebar={setIsMobileSidebar}
workspaceSlug={workspaceSlug}
projectId={projectId}
projectIdentifier={currentProjectDetails?.identifier}
inboxIssueId={inboxId}
<div className="vertical-scrollbar scrollbar-md h-full w-full overflow-hidden overflow-y-auto">
{filteredRequestIds.length > 0 ? (
filteredRequestIds.map((requestId) => (
<ExternalContoursListItem
key={requestId}
setIsMobileSidebar={setIsMobileSidebar}
workspaceSlug={workspaceSlug}
projectId={projectId}
requestId={requestId}
/>
))
) : (
<div className="flex h-full w-full items-center justify-center">
{currentTab === EInboxIssueCurrentTab.OPEN ? (
<EmptyStateDetailed
assetKey="inbox"
title={t("external_contours_page.empty_state.open_title")}
description={t("external_contours_page.empty_state.open_description")}
assetClassName="size-20"
rootClassName="px-page-x"
/>
) : (
<EmptyStateDetailed
assetKey="inbox"
title={t("external_contours_page.empty_state.closed_title")}
description={t("external_contours_page.empty_state.closed_description")}
assetClassName="size-20"
className="px-10"
/>
))
) : (
<div className="flex h-full w-full items-center justify-center">
{getAppliedFiltersCount > 0 ? (
<EmptyStateDetailed
assetKey="search"
title={t("external_contours_page.empty_state.filtered_title")}
description={t("external_contours_page.empty_state.filtered_description")}
assetClassName="size-20"
rootClassName="px-page-x"
/>
) : currentTab === EInboxIssueCurrentTab.OPEN ? (
<EmptyStateDetailed
assetKey="inbox"
title={t("external_contours_page.empty_state.open_title")}
description={t("external_contours_page.empty_state.open_description")}
assetClassName="size-20"
rootClassName="px-page-x"
/>
) : (
<EmptyStateDetailed
assetKey="inbox"
title={t("external_contours_page.empty_state.closed_title")}
description={t("external_contours_page.empty_state.closed_description")}
assetClassName="size-20"
className="px-10"
/>
)}
</div>
)}
<div ref={setElementRef}>
{inboxIssuePaginationInfo?.next_page_results && (
<Loader className="mx-auto w-full space-y-4 px-2 py-4">
<Loader.Item height="64px" width="w-100" />
<Loader.Item height="64px" width="w-100" />
</Loader>
)}
</div>
</div>
)}
)}
</div>
</div>
</div>
);
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useTranslation } from "@plane/i18n";
import { StateGroupIcon } from "@plane/propel/icons";
import type { TExternalContourRequest } from "@plane/types";
type Props = {
request: TExternalContourRequest;
iconSize?: string;
};
export function ExternalContourStatePill(props: Props) {
const { request, iconSize = "size-3.5" } = props;
const { t } = useTranslation();
const state = request.issue.state_detail;
return (
<div className="inline-flex items-center gap-1 rounded-sm border border-subtle bg-layer-2 px-1.5 py-0.5 text-11 font-medium text-secondary">
<StateGroupIcon
stateGroup={state?.group ?? "backlog"}
color={state?.color}
className={`${iconSize} shrink-0`}
/>
<span className="whitespace-nowrap">{state?.name || t("state")}</span>
</div>
);
}