From 390bcdbf389531eb1bb3d6db0fc66378d47380e0 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sat, 18 Apr 2026 21:04:29 +0300 Subject: [PATCH] =?UTF-8?q?UI=20-=20=D0=9C=D0=95=D0=96=D0=9F=D0=A0=D0=9E?= =?UTF-8?q?=D0=95=D0=9A=D0=A2=D0=9D=D0=90=D0=AF=20=D0=9A=D0=9E=D0=9C=D0=9C?= =?UTF-8?q?=D0=A3=D0=9D=D0=98=D0=9A=D0=90=D0=A6=D0=98=D0=AF:=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=B4=20=D0=B2=D0=BD=D0=B5=D1=88?= =?UTF-8?q?=D0=BD=D0=B8=D1=85=20=D0=BA=D0=BE=D0=BD=D1=82=D1=83=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=BD=D0=B0=20shell=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BB?= =?UTF-8?q?=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[projectId]/external-contours/page.tsx | 85 +++++--- .../external-contours/content-root.tsx | 85 ++++++++ .../external-contours/create-modal.tsx | 30 +++ .../external-contours/create-properties.tsx | 108 +++++++++ .../external-contours/create-root.tsx | 139 ++++++++++++ .../projects/external-contours/header.tsx | 69 ++++-- .../external-contours/issue-header.tsx | 175 +++++++++++++++ .../external-contours/issue-properties.tsx | 178 +++++++++++++++ .../projects/external-contours/issue-root.tsx | 206 ++++++++++++++++++ .../projects/external-contours/list-item.tsx | 129 +++++++++++ .../projects/external-contours/root.tsx | 110 ++++++++++ .../projects/external-contours/sidebar.tsx | 164 ++++++++++++++ .../projects/settings/intake/header.tsx | 2 +- .../inbox/content/inbox-issue-header.tsx | 4 +- .../content/inbox-issue-mobile-header.tsx | 26 ++- .../inbox/content/issue-properties.tsx | 26 ++- .../components/inbox/content/issue-root.tsx | 16 +- .../core/components/inbox/sidebar/root.tsx | 5 +- .../i18n/src/locales/en/translations.ts | 60 ++++- .../i18n/src/locales/ru/translations.ts | 65 +++++- 20 files changed, 1586 insertions(+), 96 deletions(-) create mode 100644 plane-src/apps/web/ce/components/projects/external-contours/content-root.tsx create mode 100644 plane-src/apps/web/ce/components/projects/external-contours/create-modal.tsx create mode 100644 plane-src/apps/web/ce/components/projects/external-contours/create-properties.tsx create mode 100644 plane-src/apps/web/ce/components/projects/external-contours/create-root.tsx create mode 100644 plane-src/apps/web/ce/components/projects/external-contours/issue-header.tsx create mode 100644 plane-src/apps/web/ce/components/projects/external-contours/issue-properties.tsx create mode 100644 plane-src/apps/web/ce/components/projects/external-contours/issue-root.tsx create mode 100644 plane-src/apps/web/ce/components/projects/external-contours/list-item.tsx create mode 100644 plane-src/apps/web/ce/components/projects/external-contours/root.tsx create mode 100644 plane-src/apps/web/ce/components/projects/external-contours/sidebar.tsx diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/external-contours/page.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/external-contours/page.tsx index dfaf35d..4e3b39b 100644 --- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/external-contours/page.tsx +++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/external-contours/page.tsx @@ -5,53 +5,74 @@ */ import { observer } from "mobx-react"; +import { useSearchParams } from "next/navigation"; +import { useTheme } from "next-themes"; +import { EInboxIssueCurrentTab } from "@plane/types"; import { useTranslation } from "@plane/i18n"; -import { TransferIcon } from "@plane/propel/icons"; +import darkIntakeAsset from "@/app/assets/empty-state/disabled-feature/intake-dark.webp?url"; +import lightIntakeAsset from "@/app/assets/empty-state/disabled-feature/intake-light.webp?url"; import { PageHead } from "@/components/core/page-title"; +import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; import { useProject } from "@/hooks/store/use-project"; +import { useUserPermissions } from "@/hooks/store/user"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { EUserPermissionsLevel } from "@plane/constants"; +import { EUserProjectRoles } from "@plane/types"; +import { ExternalContoursRoot } from "@/plane-web/components/projects/external-contours/root"; import type { Route } from "./+types/page"; -function ProjectExternalContoursPage(_props: Route.ComponentProps) { +function ProjectExternalContoursPage({ params }: Route.ComponentProps) { + const router = useAppRouter(); + const { workspaceSlug, projectId } = params; + const searchParams = useSearchParams(); + const navigationTab = searchParams.get("currentTab"); + const inboxIssueId = searchParams.get("inboxIssueId"); + const { resolvedTheme } = useTheme(); const { t } = useTranslation(); const { currentProjectDetails } = useProject(); + const { allowPermissions } = useUserPermissions(); + const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); + const resolvedPath = resolvedTheme === "light" ? lightIntakeAsset : darkIntakeAsset; + + if (currentProjectDetails?.inbox_view === false) + return ( +
+ { + router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`); + }, + disabled: !canPerformEmptyStateActions, + }} + /> +
+ ); const pageTitle = currentProjectDetails?.name ? t("external_contours_page.page_label", { workspace: currentProjectDetails.name }) : t("external_contours_page.page_label", { workspace: "NODE.DC" }); + const currentNavigationTab = navigationTab + ? navigationTab === "open" + ? EInboxIssueCurrentTab.OPEN + : EInboxIssueCurrentTab.CLOSED + : undefined; + return (
-
-
-
-
- -
-
-

{t("external_contours_page.empty_state.title")}

-

- {t("external_contours_page.empty_state.description")} -

-
-
-
- -
-
-

- {t("external_contours_page.empty_state.request_title")} -

-

{t("external_contours_page.empty_state.request_description")}

-
- -
-

- {t("external_contours_page.empty_state.list_title")} -

-

{t("external_contours_page.empty_state.list_description")}

-
-
+
+
); diff --git a/plane-src/apps/web/ce/components/projects/external-contours/content-root.tsx b/plane-src/apps/web/ce/components/projects/external-contours/content-root.tsx new file mode 100644 index 0000000..dfc0da9 --- /dev/null +++ b/plane-src/apps/web/ce/components/projects/external-contours/content-root.tsx @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +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 { useUser, useUserPermissions } from "@/hooks/store/user"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { ExternalContoursIssueActionsHeader } from "./issue-header"; +import { ExternalContoursIssueMainContent } from "./issue-root"; + +type Props = { + workspaceSlug: string; + projectId: string; + inboxIssueId: string; + isMobileSidebar: boolean; + setIsMobileSidebar: (value: boolean) => void; +}; + +export const ExternalContoursContentRoot = observer(function ExternalContoursContentRoot(props: Props) { + const { workspaceSlug, projectId, inboxIssueId, isMobileSidebar, setIsMobileSidebar } = props; + const router = useAppRouter(); + const [isSubmitting, setIsSubmitting] = useState("saved"); + const { data: currentUser } = useUser(); + const { currentTab, fetchInboxIssueById, getIssueInboxByIssueId, getIsIssueAvailable } = useProjectInbox(); + const inboxIssue = getIssueInboxByIssueId(inboxIssueId); + const { allowPermissions, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions(); + + const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || ""); + + useEffect(() => { + if (!isIssueAvailable && inboxIssueId) { + router.replace(`/${workspaceSlug}/projects/${projectId}/external-contours?currentTab=${currentTab}`); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isIssueAvailable]); + + useSWR( + workspaceSlug && projectId && inboxIssueId ? `PROJECT_EXTERNAL_CONTOUR_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${inboxIssueId}` : null, + workspaceSlug && projectId && inboxIssueId ? () => fetchInboxIssueById(workspaceSlug, projectId, inboxIssueId) : null, + { revalidateOnFocus: false, revalidateIfStale: false } + ); + + const isEditable = + allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId) || + inboxIssue?.issue.created_by === currentUser?.id; + + const isGuest = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId) === EUserPermissions.GUEST; + const isOwner = inboxIssue?.issue.created_by === currentUser?.id; + const readOnly = !isOwner && isGuest; + + if (!inboxIssue) return <>; + + return ( +
+
+ +
+ + + +
+ ); +}); diff --git a/plane-src/apps/web/ce/components/projects/external-contours/create-modal.tsx b/plane-src/apps/web/ce/components/projects/external-contours/create-modal.tsx new file mode 100644 index 0000000..a28c972 --- /dev/null +++ b/plane-src/apps/web/ce/components/projects/external-contours/create-modal.tsx @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; +import { ExternalContoursCreateRoot } from "./create-root"; + +type Props = { + workspaceSlug: string; + projectId: string; + modalState: boolean; + handleModalClose: () => void; +}; + +export function ExternalContourCreateModalRoot(props: Props) { + const { workspaceSlug, projectId, modalState, handleModalClose } = props; + + return ( + + + + ); +} diff --git a/plane-src/apps/web/ce/components/projects/external-contours/create-properties.tsx b/plane-src/apps/web/ce/components/projects/external-contours/create-properties.tsx new file mode 100644 index 0000000..57bd902 --- /dev/null +++ b/plane-src/apps/web/ce/components/projects/external-contours/create-properties.tsx @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { useMemo } from "react"; +import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; +import { Badge } from "@plane/propel/badge"; +import type { TIssue, TIssuePriorities } from "@plane/types"; +import { renderFormattedPayloadDate } from "@plane/utils"; +import { DateDropdown } from "@/components/dropdowns/date"; +import { MemberDropdown } from "@/components/dropdowns/member/dropdown"; +import { PriorityDropdown } from "@/components/dropdowns/priority"; +import { ProjectDropdown } from "@/components/dropdowns/project/dropdown"; +import { IssueLabelSelect } from "@/components/issues/select"; +import { useProject } from "@/hooks/store/use-project"; + +type Props = { + currentProjectId: string; + currentProjectName?: string; + data: Partial & { target_project_id?: string | null; priority?: TIssuePriorities }; + handleData: (issueKey: keyof Props["data"], issueValue: Props["data"][keyof Props["data"]]) => void; +}; + +export const ExternalContoursCreateProperties = observer(function ExternalContoursCreateProperties(props: Props) { + const { currentProjectId, currentProjectName, data, handleData } = props; + const { t } = useTranslation(); + const { getProjectById } = useProject(); + + const selectedTargetProject = useMemo( + () => (data.target_project_id ? getProjectById(data.target_project_id) : undefined), + [data.target_project_id, getProjectById] + ); + + return ( +
+
+ {t("external_contours_page.form.source_project")} + {currentProjectName || "NODE.DC"} +
+ +
+ { + if (!Array.isArray(value)) { + handleData("target_project_id", value); + handleData("assignee_ids", []); + handleData("label_ids", []); + } + }} + multiple={false} + buttonVariant="border-with-text" + placeholder={t("external_contours_page.form.target_project")} + renderCondition={(projectId) => projectId !== currentProjectId} + /> +
+ + {selectedTargetProject && ( +
+ {t("external_contours_page.form.selected_target")} + {selectedTargetProject.name} +
+ )} + +
+ handleData("priority", priority)} + buttonVariant="border-with-text" + /> +
+ +
+ handleData("assignee_ids", assigneeIds)} + buttonVariant={(data.assignee_ids || []).length > 0 ? "transparent-without-text" : "border-with-text"} + buttonClassName={(data.assignee_ids || []).length > 0 ? "hover:bg-transparent" : ""} + placeholder={t("external_contours_page.form.assignee")} + disabled={!data.target_project_id} + multiple + /> +
+ +
+ handleData("label_ids", labelIds)} + projectId={data.target_project_id ?? undefined} + disabled={!data.target_project_id} + /> +
+ +
+ handleData("target_date", date ? renderFormattedPayloadDate(date) : "")} + buttonVariant="border-with-text" + placeholder={t("external_contours_page.form.due_date")} + /> +
+
+ ); +}); diff --git a/plane-src/apps/web/ce/components/projects/external-contours/create-root.tsx b/plane-src/apps/web/ce/components/projects/external-contours/create-root.tsx new file mode 100644 index 0000000..1cafe3a --- /dev/null +++ b/plane-src/apps/web/ce/components/projects/external-contours/create-root.tsx @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import type { FormEvent } from "react"; +import { useRef, useState } from "react"; +import { observer } from "mobx-react"; +import type { EditorRefApi } from "@plane/editor"; +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 { ToggleSwitch } from "@plane/ui"; +import { useProject } from "@/hooks/store/use-project"; +import { useWorkspace } from "@/hooks/store/use-workspace"; +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"; + +const defaultIssueData: Partial & { target_project_id?: string | null } = { + id: undefined, + name: "", + description_html: "", + priority: "none", + target_project_id: null, + label_ids: [], + assignee_ids: [], + target_date: "", +}; + +type Props = { + workspaceSlug: string; + projectId: string; + handleModalClose: () => void; +}; + +export const ExternalContoursCreateRoot = observer(function ExternalContoursCreateRoot(props: Props) { + const { workspaceSlug, projectId, handleModalClose } = props; + const { t } = useTranslation(); + const { getWorkspaceBySlug } = useWorkspace(); + const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id; + const { currentProjectDetails } = useProject(); + const descriptionEditorRef = useRef(null); + const [createMore, setCreateMore] = useState(false); + const [formSubmitting, setFormSubmitting] = useState(false); + const [formData, setFormData] = useState & { target_project_id?: string | null }>(defaultIssueData); + + const handleFormData = (issueKey: T, issueValue: (typeof formData)[T]) => { + setFormData((current) => ({ ...current, [issueKey]: issueValue })); + }; + + const isTitleLengthMoreThan255Character = formData?.name ? formData.name.length > 255 : false; + const canSubmit = !!formData.name?.trim() && !!formData.target_project_id; + + const handleFormSubmit = async (event: FormEvent) => { + event.preventDefault(); + + if (!descriptionEditorRef.current?.isEditorReadyToDiscard()) { + setToast({ + type: TOAST_TYPE.ERROR, + title: t("error"), + message: t("editor_is_not_ready_to_discard_changes"), + }); + return; + } + + 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(); + } + }; + + if (!workspaceSlug || !projectId || !workspaceId) return <>; + + return ( +
+
+
+
+
+

{t("external_contours_page.modal.title")}

+
+
+ + {}} + /> + +
+
+
+
setCreateMore((prevData) => !prevData)} + role="button" + tabIndex={0} + > + {}} size="sm" /> + {t("create_more")} +
+
+ + +
+
+
+
+
+ ); +}); diff --git a/plane-src/apps/web/ce/components/projects/external-contours/header.tsx b/plane-src/apps/web/ce/components/projects/external-contours/header.tsx index cc600c7..8f11831 100644 --- a/plane-src/apps/web/ce/components/projects/external-contours/header.tsx +++ b/plane-src/apps/web/ce/components/projects/external-contours/header.tsx @@ -4,36 +4,77 @@ * See the LICENSE file for details. */ +import { useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; +import { RefreshCcw } from "lucide-react"; +import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { Button } from "@plane/propel/button"; 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 { useUserPermissions } from "@/hooks/store/user"; import { CommonProjectBreadcrumbs } from "@/plane-web/components/breadcrumbs/common"; +import { ExternalContourCreateModalRoot } from "./create-modal"; export const ProjectExternalContoursHeader = observer(function ProjectExternalContoursHeader() { + const [createIssueModal, setCreateIssueModal] = useState(false); const { workspaceSlug, projectId } = useParams(); const { t } = useTranslation(); + const { allowPermissions } = useUserPermissions(); + const { currentProjectDetails, loader: currentProjectDetailsLoader } = useProject(); + const { loader } = useProjectInbox(); + + const isAuthorized = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + EUserPermissionsLevel.PROJECT + ); return (
- - - } - isLast - /> - } - isLast - /> - +
+ + + } + isLast + /> + } + isLast + /> + + + {loader === "pagination-loading" && ( +
+ +

{t("syncing")}...

+
+ )} +
+ + {currentProjectDetails?.inbox_view && workspaceSlug && projectId && isAuthorized ? ( +
+ setCreateIssueModal(false)} + /> + +
+ ) : null} +
); }); diff --git a/plane-src/apps/web/ce/components/projects/external-contours/issue-header.tsx b/plane-src/apps/web/ce/components/projects/external-contours/issue-header.tsx new file mode 100644 index 0000000..5ab5465 --- /dev/null +++ b/plane-src/apps/web/ce/components/projects/external-contours/issue-header.tsx @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { useCallback, useEffect } from "react"; +import { observer } from "mobx-react"; +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 { TOAST_TYPE, setToast } from "@plane/propel/toast"; +import type { TNameDescriptionLoader } from "@plane/types"; +import { EInboxIssueCurrentTab } 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 { useAppRouter } from "@/hooks/use-app-router"; +import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store"; +import { InboxIssueStatus } from "@/components/inbox/inbox-issue-status"; + +type Props = { + workspaceSlug: string; + projectId: string; + inboxIssue: IInboxIssueStore | undefined; + 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 { t } = useTranslation(); + const router = useAppRouter(); + const { currentTab, filteredInboxIssueIds } = useProjectInbox(); + const { getProjectById } = useProject(); + + const issue = inboxIssue?.issue; + const currentInboxIssueId = issue?.id; + + const redirectToRelativeIssue = useCallback( + (direction: "next" | "prev") => { + if (!filteredInboxIssueIds || !currentInboxIssueId) return; + const currentIssueIndex = filteredInboxIssueIds.findIndex((issueId) => issueId === currentInboxIssueId); + const nextIssueIndex = + direction === "next" + ? (currentIssueIndex + 1) % filteredInboxIssueIds.length + : (currentIssueIndex - 1 + filteredInboxIssueIds.length) % filteredInboxIssueIds.length; + const nextIssueId = filteredInboxIssueIds[nextIssueIndex]; + if (!nextIssueId) return; + router.push(`/${workspaceSlug}/projects/${projectId}/external-contours?currentTab=${currentTab}&inboxIssueId=${nextIssueId}`); + }, + [currentInboxIssueId, currentTab, filteredInboxIssueIds, projectId, router, workspaceSlug] + ); + + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "ArrowUp") redirectToRelativeIssue("prev"); + if (event.key === "ArrowDown") redirectToRelativeIssue("next"); + }; + + document.addEventListener("keydown", onKeyDown); + return () => document.removeEventListener("keydown", onKeyDown); + }, [redirectToRelativeIssue]); + + if (!issue || !inboxIssue) return null; + + const workItemLink = generateWorkItemLink({ + workspaceSlug: workspaceSlug?.toString(), + projectId: issue.project_id, + issueId: currentInboxIssueId, + projectIdentifier: getProjectById(issue.project_id)?.identifier, + 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({ + type: TOAST_TYPE.SUCCESS, + title: t("common.link_copied"), + message: t("common.copied_to_clipboard"), + }) + ); + + const isOpenTab = currentTab === EInboxIssueCurrentTab.OPEN; + + return ( + <> + +
+ {issue?.project_id && issue.sequence_id && ( +

+ {getProjectById(issue.project_id)?.identifier}-{issue.sequence_id} +

+ )} + +
+ +
+
+ +
+
+ redirectToRelativeIssue("prev")} /> + redirectToRelativeIssue("next")} /> +
+ +
+ {isOpenTab ? ( + <> + + + + ) : ( + <> + + + + )} + + + router.push(workItemLink)} target="_self"> + + +
+
+
+ +
+ setIsMobileSidebar(!isMobileSidebar)} + className={`my-auto mr-2 h-4 w-4 flex-shrink-0 ${isMobileSidebar ? "text-accent-primary" : "text-secondary"}`} + /> +
+ +
+ + +
+
+
+ + ); +}); diff --git a/plane-src/apps/web/ce/components/projects/external-contours/issue-properties.tsx b/plane-src/apps/web/ce/components/projects/external-contours/issue-properties.tsx new file mode 100644 index 0000000..0ce071f --- /dev/null +++ b/plane-src/apps/web/ce/components/projects/external-contours/issue-properties.tsx @@ -0,0 +1,178 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +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 { DateDropdown } from "@/components/dropdowns/date"; +import { MemberDropdown } from "@/components/dropdowns/member/dropdown"; +import { PriorityDropdown } from "@/components/dropdowns/priority"; +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; + issueOperations: TIssueOperations; + isEditable: boolean; + duplicateIssueDetails: TInboxDuplicateIssueDetails | undefined; +}; + +export const ExternalContoursIssueContentProperties = observer(function ExternalContoursIssueContentProperties(props: Props) { + const { workspaceSlug, projectId, issue, issueOperations, isEditable, duplicateIssueDetails } = 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 ( +
+
+
{t("external_contours_page.properties.section_title")}
+
+
+
+
+ + {t("external_contours_page.properties.target_contour")} +
+
+ {t("external_contours_page.properties.target_contour_placeholder")} +
+
+ +
+
+ + {t("assignees")} +
+ issue?.id && issueOperations.update(workspaceSlug, projectId, issue.id, { assignee_ids: val })} + disabled={!isEditable} + projectId={projectId?.toString() ?? ""} + placeholder={t("assignee")} + multiple + buttonVariant={(issue?.assignee_ids || []).length > 0 ? "transparent-without-text" : "transparent-with-text"} + className="group w-3/5 flex-grow" + buttonContainerClassName="w-full text-left" + buttonClassName={`text-13 justify-between ${(issue?.assignee_ids || []).length > 0 ? "" : "text-placeholder"}`} + hideIcon={issue.assignee_ids?.length === 0} + dropdownArrow + dropdownArrowClassName="hidden h-3.5 w-3.5 group-hover:inline" + /> +
+ +
+
+ + {t("priority")} +
+ issue?.id && issueOperations.update(workspaceSlug, projectId, issue.id, { priority: val })} + disabled={!isEditable} + buttonVariant="border-with-text" + className="w-3/5 flex-grow rounded-sm px-2 hover:bg-layer-1" + buttonContainerClassName="w-full text-left" + buttonClassName="h-auto w-min whitespace-nowrap" + /> +
+
+
+ +
+
+
+
+ + {t("due_date")} +
+ + issue?.id && + issueOperations.update(workspaceSlug, projectId, issue.id, { + target_date: val ? renderFormattedPayloadDate(val) : null, + }) + } + minDate={minDate ?? undefined} + disabled={!isEditable} + buttonVariant="transparent-with-text" + className="group w-3/5 flex-grow" + buttonContainerClassName="w-full text-left" + buttonClassName={`text-13 ${issue?.target_date ? "" : "text-placeholder"}`} + hideIcon + clearIconClassName="hidden h-3 w-3 group-hover:inline" + /> +
+ +
+
+ + {t("labels")} +
+
+ {issue?.id && ( + issue?.id && issueOperations.update(workspaceSlug, projectId, issue.id, { label_ids: val })} + /> + )} +
+
+ + {duplicateIssueDetails && ( +
+
+ + {t("external_contours_page.properties.duplicate_of")} +
+ router.push(duplicateWorkItemLink)} target="_self"> + + + {`${currentProjectDetails?.identifier}-${duplicateIssueDetails?.sequence_id}`} + + + +
+ )} +
+
+
+
+ ); +}); diff --git a/plane-src/apps/web/ce/components/projects/external-contours/issue-root.tsx b/plane-src/apps/web/ce/components/projects/external-contours/issue-root.tsx new file mode 100644 index 0000000..b2e2c01 --- /dev/null +++ b/plane-src/apps/web/ce/components/projects/external-contours/issue-root.tsx @@ -0,0 +1,206 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import type { Dispatch, SetStateAction } from "react"; +import { useEffect, useMemo, useRef } from "react"; +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 { 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 { 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 { 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 { ExternalContoursIssueContentProperties } from "./issue-properties"; + +const intakeWorkItemVersionService = new IntakeWorkItemVersionService(); + +type Props = { + workspaceSlug: string; + projectId: string; + inboxIssue: IInboxIssueStore; + isEditable: boolean; + isSubmitting: TNameDescriptionLoader; + setIsSubmitting: Dispatch>; +}; + +export const ExternalContoursIssueMainContent = observer(function ExternalContoursIssueMainContent(props: Props) { + const { workspaceSlug, projectId, inboxIssue, isEditable, isSubmitting, setIsSubmitting } = props; + const { t } = useTranslation(); + const editorRef = useRef(null); + const { data: currentUser } = useUser(); + const { getUserDetails } = useMember(); + const { loader } = useProjectInbox(); + const { getProjectById } = useProject(); + const { removeIssue, archiveIssue } = useIssueDetail(); + const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting"); + + useEffect(() => { + if (isSubmitting === "submitted") { + setShowAlert(false); + setTimeout(async () => setIsSubmitting("saved"), 3000); + } else if (isSubmitting === "submitting") { + setShowAlert(true); + } + }, [isSubmitting, setIsSubmitting, setShowAlert]); + + const issue = inboxIssue.issue; + const projectDetails = issue?.project_id ? getProjectById(issue.project_id) : undefined; + + const { duplicateIssues } = useDebouncedDuplicateIssues(workspaceSlug, projectDetails?.workspace.toString(), projectId, { + 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); + 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); + 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) => { + try { + await inboxIssue.updateIssue(data); + } 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); + } + }, + }), + [archiveIssue, inboxIssue, projectId, removeIssue, workspaceSlug] + ); + + if (!issue || !issue.project_id || !issue.id) return <>; + + return ( + <> +
+ {duplicateIssues.length > 0 && ( + + )} + setIsSubmitting(value)} + issueOperations={issueOperations} + disabled={!isEditable} + value={issue.name} + containerClassName="-ml-3" + /> + + {loader === "issue-loading" || issue.description_html === undefined ? ( + + ) : ( +

" : issue.description_html} + key={issue.id} + onSubmit={async (value, isMigrationUpdate) => { + await issueOperations.update(workspaceSlug, issue.project_id, issue.id, { + description_html: value.description_html, + ...(isMigrationUpdate ? { skip_activity: "true" } : {}), + }); + }} + projectId={issue.project_id} + setIsSubmitting={(value) => setIsSubmitting(value)} + workspaceSlug={workspaceSlug} + /> + )} + +
+ {currentUser && ( + + )} + {isEditable && ( + intakeWorkItemVersionService.listDescriptionVersions(workspaceSlug, projectId, issueId), + retrieveDescriptionVersion: (issueId, versionId) => + intakeWorkItemVersionService.retrieveDescriptionVersion(workspaceSlug, projectId, issueId, versionId), + }} + handleRestore={(descriptionHTML) => editorRef.current?.setEditorValue(descriptionHTML, true)} + projectId={projectId} + workspaceSlug={workspaceSlug} + /> + )} +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ + ); +}); diff --git a/plane-src/apps/web/ce/components/projects/external-contours/list-item.tsx b/plane-src/apps/web/ce/components/projects/external-contours/list-item.tsx new file mode 100644 index 0000000..31c9f28 --- /dev/null +++ b/plane-src/apps/web/ce/components/projects/external-contours/list-item.tsx @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import type { MouseEvent } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +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 { usePlatformOS } from "@/hooks/use-platform-os"; +import { InboxSourcePill } from "@/plane-web/components/inbox/source-pill"; +import { InboxIssueStatus } from "@/components/inbox/inbox-issue-status"; + +type Props = { + workspaceSlug: string; + projectId: string; + projectIdentifier?: string; + inboxIssueId: string; + setIsMobileSidebar: (value: boolean) => void; +}; + +export const ExternalContoursListItem = observer(function ExternalContoursListItem(props: Props) { + const { workspaceSlug, projectId, inboxIssueId, projectIdentifier, setIsMobileSidebar } = props; + const searchParams = useSearchParams(); + const selectedInboxIssueId = searchParams.get("inboxIssueId"); + const { t } = useTranslation(); + const { currentTab, getIssueInboxByIssueId } = useProjectInbox(); + const { projectLabels } = useLabel(); + const { isMobile } = usePlatformOS(); + const { getUserDetails } = useMember(); + const inboxIssue = getIssueInboxByIssueId(inboxIssueId); + const issue = inboxIssue?.issue; + + const handleIssueRedirection = (event: MouseEvent, currentIssueId: string | undefined) => { + if (selectedInboxIssueId === currentIssueId) event.preventDefault(); + setIsMobileSidebar(false); + }; + + if (!issue) return <>; + + const createdByDetails = issue?.created_by ? getUserDetails(issue?.created_by) : undefined; + + return ( + handleIssueRedirection(e, issue.id)} + > + +
+
+
+ {projectIdentifier}-{issue.sequence_id} +
+
+ {inboxIssue.source && } + {inboxIssue.status !== -2 && } +
+
+

{issue.name}

+
+ +
+
+ +
{renderFormattedDate(issue.created_at ?? "")}
+
+ +
+ + {issue.priority && ( + + + + )} + + {issue.label_ids && issue.label_ids.length > 3 ? ( +
+ + {`${issue.label_ids.length} ${t("labels").toLowerCase()}`} +
+ ) : ( + <> + {(issue.label_ids ?? []).map((labelId) => { + const labelDetails = projectLabels?.find((l) => l.id === labelId); + if (!labelDetails) return null; + return ( +
+ + {labelDetails.name} +
+ ); + })} + + )} +
+ {createdByDetails && createdByDetails.email?.includes("intake@plane.so") ? ( + + ) : createdByDetails ? ( + + ) : null} +
+ + + ); +}); diff --git a/plane-src/apps/web/ce/components/projects/external-contours/root.tsx b/plane-src/apps/web/ce/components/projects/external-contours/root.tsx new file mode 100644 index 0000000..bcb6114 --- /dev/null +++ b/plane-src/apps/web/ce/components/projects/external-contours/root.tsx @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import { PanelLeft } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; +import { TransferIcon } from "@plane/propel/icons"; +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 { ExternalContoursContentRoot } from "./content-root"; +import { ExternalContoursSidebar } from "./sidebar"; + +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 [isMobileSidebar, setIsMobileSidebar] = useState(true); + const { t } = useTranslation(); + const { loader, error, currentTab, currentInboxProjectId, handleCurrentTab, fetchInboxIssues } = useProjectInbox(); + + useEffect(() => { + if (!inboxAccessible || !workspaceSlug || !projectId) return; + + const hasProjectChanged = currentInboxProjectId && currentInboxProjectId !== 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); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [inboxAccessible, workspaceSlug, projectId]); + + if (loader === "init-loading") { + return ( +
+ +
+ ); + } + + if (error && error?.status === "init-error") { + return ( +
+ +
{error?.message}
+
+ ); + } + + return ( + <> + {!inboxIssueId && ( +
+ setIsMobileSidebar(!isMobileSidebar)} + className={cn("h-4 w-4", isMobileSidebar ? "text-accent-primary" : "text-secondary")} + /> +
+ )} +
+
+ +
+ + {inboxIssueId ? ( + + ) : ( + + )} +
+ + ); +}); diff --git a/plane-src/apps/web/ce/components/projects/external-contours/sidebar.tsx b/plane-src/apps/web/ce/components/projects/external-contours/sidebar.tsx new file mode 100644 index 0000000..30d2feb --- /dev/null +++ b/plane-src/apps/web/ce/components/projects/external-contours/sidebar.tsx @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { useCallback, useEffect, useRef, useState } 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 { 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 { 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 = { + workspaceSlug: string; + projectId: string; + inboxIssueId: string | undefined; + setIsMobileSidebar: (value: boolean) => void; +}; + +const tabNavigationOptions: { key: TInboxIssueCurrentTab; i18n_label: string }[] = [ + { key: EInboxIssueCurrentTab.OPEN, i18n_label: "external_contours_page.tabs.open" }, + { key: EInboxIssueCurrentTab.CLOSED, i18n_label: "external_contours_page.tabs.closed" }, +]; + +export const ExternalContoursSidebar = observer(function ExternalContoursSidebar(props: Props) { + const { workspaceSlug, projectId, inboxIssueId, setIsMobileSidebar } = props; + const router = useAppRouter(); + const containerRef = useRef(null); + const [elementRef, setElementRef] = useState(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%"); + + useEffect(() => { + if (workspaceSlug && projectId && currentTab && filteredInboxIssueIds.length > 0 && inboxIssueId === undefined) { + router.push( + `/${workspaceSlug}/projects/${projectId}/external-contours?currentTab=${currentTab}&inboxIssueId=${filteredInboxIssueIds[0]}` + ); + } + }, [currentTab, filteredInboxIssueIds, inboxIssueId, projectId, router, workspaceSlug]); + + return ( +
+
+
+ {tabNavigationOptions.map((option) => ( +
{ + if (currentTab !== option.key) { + handleCurrentTab(workspaceSlug, projectId, option.key); + router.push(`/${workspaceSlug}/projects/${projectId}/external-contours?currentTab=${option.key}`); + } + }} + > +
{t(option.i18n_label)}
+ {option.key === EInboxIssueCurrentTab.OPEN && currentTab === option.key && ( +
+ {inboxIssuePaginationInfo?.total_results || 0} +
+ )} +
+
+ ))} +
+ +
+
+ + + + {loader === "filter-loading" && !inboxIssuePaginationInfo?.next_page_results ? ( + + ) : ( +
+ {filteredInboxIssueIds.length > 0 ? ( + filteredInboxIssueIds.map((inboxId) => ( + + )) + ) : ( +
+ {getAppliedFiltersCount > 0 ? ( + + ) : currentTab === EInboxIssueCurrentTab.OPEN ? ( + + ) : ( + + )} +
+ )} +
+ {inboxIssuePaginationInfo?.next_page_results && ( + + + + + )} +
+
+ )} +
+
+ ); +}); diff --git a/plane-src/apps/web/ce/components/projects/settings/intake/header.tsx b/plane-src/apps/web/ce/components/projects/settings/intake/header.tsx index c60466e..9bb0961 100644 --- a/plane-src/apps/web/ce/components/projects/settings/intake/header.tsx +++ b/plane-src/apps/web/ce/components/projects/settings/intake/header.tsx @@ -51,7 +51,7 @@ export const ProjectInboxHeader = observer(function ProjectInboxHeader() { } isLast diff --git a/plane-src/apps/web/core/components/inbox/content/inbox-issue-header.tsx b/plane-src/apps/web/core/components/inbox/content/inbox-issue-header.tsx index 5243521..d744f25 100644 --- a/plane-src/apps/web/core/components/inbox/content/inbox-issue-header.tsx +++ b/plane-src/apps/web/core/components/inbox/content/inbox-issue-header.tsx @@ -217,7 +217,7 @@ export const InboxIssueActionsHeader = observer(function InboxIssueActionsHeader else { setToast({ type: TOAST_TYPE.ERROR, - title: "Permission denied", + title: t("permission_denied"), message: errorMessage, }); } @@ -407,7 +407,7 @@ export const InboxIssueActionsHeader = observer(function InboxIssueActionsHeader handleActionWithPermission( isProjectAdmin, () => setSelectDuplicateIssue(true), - "Only project admins can mark work item as duplicate" + t("inbox_issue.errors.duplicate_permission") ) } > diff --git a/plane-src/apps/web/core/components/inbox/content/inbox-issue-mobile-header.tsx b/plane-src/apps/web/core/components/inbox/content/inbox-issue-mobile-header.tsx index bde8166..93f9cda 100644 --- a/plane-src/apps/web/core/components/inbox/content/inbox-issue-mobile-header.tsx +++ b/plane-src/apps/web/core/components/inbox/content/inbox-issue-mobile-header.tsx @@ -6,6 +6,7 @@ import { observer } from "mobx-react"; import { Clock, FileStack, MoreHorizontal, PanelLeft, MoveRight } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; import { IconButton, getIconButtonStyling } from "@plane/propel/icon-button"; import { LinkIcon, @@ -79,6 +80,7 @@ export const InboxIssueActionsMobileHeader = observer(function InboxIssueActions isProjectAdmin, handleActionWithPermission, } = props; + const { t } = useTranslation(); const router = useAppRouter(); const { getProjectIdentifierById } = useProject(); @@ -143,7 +145,7 @@ export const InboxIssueActionsMobileHeader = observer(function InboxIssueActions
- Copy work item link + {t("inbox_issue.actions.copy")}
)} @@ -151,7 +153,7 @@ export const InboxIssueActionsMobileHeader = observer(function InboxIssueActions router.push(workItemLink)}>
- Open work item + {t("inbox_issue.actions.open")}
)} @@ -161,13 +163,15 @@ export const InboxIssueActionsMobileHeader = observer(function InboxIssueActions handleActionWithPermission( isProjectAdmin, handleIssueSnoozeAction, - "Only project admins can snooze/Un-snooze work items" + t("inbox_issue.errors.snooze_permission") ) } >
- {inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0 ? "Un-snooze" : "Snooze"} + {inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0 + ? t("inbox_issue.actions.unsnooze") + : t("inbox_issue.actions.snooze")}
)} @@ -177,13 +181,13 @@ export const InboxIssueActionsMobileHeader = observer(function InboxIssueActions handleActionWithPermission( isProjectAdmin, () => setSelectDuplicateIssue(true), - "Only project admins can mark work items as duplicate" + t("inbox_issue.errors.duplicate_permission") ) } >
- Mark as duplicate + {t("inbox_issue.actions.mark_as_duplicate")}
)} @@ -193,13 +197,13 @@ export const InboxIssueActionsMobileHeader = observer(function InboxIssueActions handleActionWithPermission( isProjectAdmin, () => setAcceptIssueModal(true), - "Only project admins can accept work items" + t("inbox_issue.errors.accept_permission") ) } >
- Accept + {t("inbox_issue.actions.accept")}
)} @@ -209,13 +213,13 @@ export const InboxIssueActionsMobileHeader = observer(function InboxIssueActions handleActionWithPermission( isProjectAdmin, () => setDeclineIssueModal(true), - "Only project admins can deny work items" + t("inbox_issue.errors.decline_permission") ) } >
- Decline + {t("inbox_issue.actions.decline")}
)} @@ -223,7 +227,7 @@ export const InboxIssueActionsMobileHeader = observer(function InboxIssueActions setDeleteIssueModal(true)}>
- Delete + {t("inbox_issue.actions.delete")}
)} diff --git a/plane-src/apps/web/core/components/inbox/content/issue-properties.tsx b/plane-src/apps/web/core/components/inbox/content/issue-properties.tsx index f355e39..b640c79 100644 --- a/plane-src/apps/web/core/components/inbox/content/issue-properties.tsx +++ b/plane-src/apps/web/core/components/inbox/content/issue-properties.tsx @@ -5,6 +5,7 @@ */ import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; import { StatePropertyIcon, MembersPropertyIcon, @@ -44,6 +45,7 @@ export const InboxIssueContentProperties = observer(function InboxIssueContentPr props; const router = useAppRouter(); + const { t } = useTranslation(); // store hooks const { currentProjectDetails } = useProject(); @@ -63,14 +65,14 @@ export const InboxIssueContentProperties = observer(function InboxIssueContentPr return (
-
Properties
+
{t("properties")}
{/* Intake State */}
- State + {t("state")}
{issue?.state_id && (
- Assignees + {t("assignees")}
0 ? "transparent-without-text" : "transparent-with-text" @@ -119,7 +121,7 @@ export const InboxIssueContentProperties = observer(function InboxIssueContentPr
- Priority + {t("priority")}
- Due date + {t("due_date")}
issue?.id && @@ -166,7 +168,7 @@ export const InboxIssueContentProperties = observer(function InboxIssueContentPr
- Labels + {t("labels")}
{issue?.id && ( @@ -187,10 +189,10 @@ export const InboxIssueContentProperties = observer(function InboxIssueContentPr {/* duplicate to*/} {duplicateIssueDetails && (
-
- - Duplicate of -
+
+ + {t("issues.properties.duplicate_of")} +
(null); // store hooks @@ -101,16 +103,16 @@ export const InboxIssueMainContent = observer(function InboxIssueMainContent(pro try { await removeIssue(workspaceSlug, projectId, _issueId); setToast({ - title: "Success!", + title: t("success"), type: TOAST_TYPE.SUCCESS, - message: "Work item deleted successfully", + message: t("inbox_issue.modals.delete.success"), }); } catch (error) { console.log("Error in deleting work item:", error); setToast({ - title: "Error!", + title: t("error"), type: TOAST_TYPE.ERROR, - message: "Work item delete failed", + message: t("something_went_wrong_please_try_again"), }); } }, @@ -119,9 +121,9 @@ export const InboxIssueMainContent = observer(function InboxIssueMainContent(pro await inboxIssue.updateIssue(data); } catch (_error) { setToast({ - title: "Work item update failed", + title: t("error"), type: TOAST_TYPE.ERROR, - message: "Work item update failed", + message: t("issue_could_not_be_updated"), }); } }, @@ -206,7 +208,7 @@ export const InboxIssueMainContent = observer(function InboxIssueMainContent(pro createdAt: issue.created_at ? new Date(issue.created_at) : new Date(), createdByDisplayName: inboxIssue.source === EInboxIssueSource.FORMS - ? "Intake Form user" + ? t("inbox_issue.source.form_user") : (getUserDetails(issue.created_by ?? "")?.display_name ?? ""), id: issue.id, isRestoreDisabled: !isEditable, diff --git a/plane-src/apps/web/core/components/inbox/sidebar/root.tsx b/plane-src/apps/web/core/components/inbox/sidebar/root.tsx index e378c76..b6a7f2b 100644 --- a/plane-src/apps/web/core/components/inbox/sidebar/root.tsx +++ b/plane-src/apps/web/core/components/inbox/sidebar/root.tsx @@ -161,11 +161,10 @@ export const InboxSidebar = observer(function InboxSidebar(props: IInboxSidebarP rootClassName="px-page-x" /> ) : ( - // TODO: Add translation diff --git a/plane-src/packages/i18n/src/locales/en/translations.ts b/plane-src/packages/i18n/src/locales/en/translations.ts index 16b067f..616d757 100644 --- a/plane-src/packages/i18n/src/locales/en/translations.ts +++ b/plane-src/packages/i18n/src/locales/en/translations.ts @@ -33,6 +33,7 @@ export default { success: "Success", warning: "Warning", info: "Info", + permission_denied: "Permission denied", close: "Close", yes: "Yes", no: "No", @@ -283,16 +284,60 @@ export default { external_contours_page: { page_label: "{workspace} - External contours", title: "External contours", + disabled: { + title: "External contours are disabled in this project", + description: "Enable the intake feature first to use cross-project communication in this project.", + button: "Open project features", + }, + header: { + add_request: "Add request", + }, + tabs: { + open: "Open", + closed: "Closed", + }, empty_state: { title: "External contours module is ready for the next stage", description: "This screen will host the cross-project request form, the sent requests list, and status pills for each routed task.", - request_title: "Send to an external contour", - request_description: - "The next stage will add the form for selecting the target project, assignee, priority, due date, and description.", - list_title: "Sent requests", - list_description: - "This area will show sent cross-project requests with their current status, a link to the target task, and later synchronization.", + detail_title: "Select a request to view its details.", + open_title: "No open requests", + open_description: "Sent and active cross-contour requests will appear here.", + closed_title: "No closed requests", + closed_description: "Completed, accepted, and declined requests will appear here.", + filtered_title: "No matching requests", + filtered_description: "No requests match the current filters.", + }, + form: { + source_project: "Source internal contour", + target_project: "Target external contour", + selected_target: "Selected contour", + assignee: "Assignee", + due_date: "Due date", + }, + modal: { + title: "Add request", + submit: "Send", + toast_title: "Routing is not connected yet", + toast_message: + "The UI now uses the intake shell. The next stage will connect backend routing and real delivery to the target contour.", + }, + properties: { + section_title: "Properties", + target_contour: "External contour", + target_contour_placeholder: "Will be available after routing is connected", + add_due_date: "Add due date", + duplicate_of: "Duplicate of", + }, + actions: { + send: "Send", + accept: "Accept", + decline: "Decline", + copy: "Copy link", + open: "Open request", + unsupported_title: "This action will be connected in the next stage", + unsupported_message: + "The “{action}” button is already placed in the right UI slot. Real routing and the reverse flow will be connected next.", }, }, deactivate_your_account: "Deactivate your account", @@ -324,6 +369,7 @@ export default { no_matching_results: "No matching results", title_is_required: "Title is required", title: "Title", + properties: "Properties", state: "State", priority: "Priority", none: "None", @@ -1101,6 +1147,7 @@ export default { snooze_permission: "Only project admins can snooze/Un-snooze work items", accept_permission: "Only project admins can accept work items", decline_permission: "Only project admins can deny work items", + duplicate_permission: "Only project admins can mark work items as duplicate", }, actions: { accept: "Accept", @@ -1115,6 +1162,7 @@ export default { }, source: { "in-app": "in-app", + form_user: "Form user", }, order_by: { created_at: "Created at", diff --git a/plane-src/packages/i18n/src/locales/ru/translations.ts b/plane-src/packages/i18n/src/locales/ru/translations.ts index d75d011..e891612 100644 --- a/plane-src/packages/i18n/src/locales/ru/translations.ts +++ b/plane-src/packages/i18n/src/locales/ru/translations.ts @@ -185,6 +185,7 @@ export default { success: "Успешно", warning: "Предупреждение", info: "Информация", + permission_denied: "Доступ запрещён", close: "Закрыть", yes: "Да", no: "Нет", @@ -439,16 +440,61 @@ export default { external_contours_page: { page_label: "{workspace} - Внешние контуры", title: "Внешние контуры", + disabled: { + title: "Внешние контуры отключены в проекте", + description: + "Чтобы использовать межпроектную коммуникацию, сначала включите модуль предложений в функциях проекта.", + button: "Открыть функции проекта", + }, + header: { + add_request: "Добавить запрос", + }, + tabs: { + open: "Открытые", + closed: "Закрытые", + }, empty_state: { title: "Модуль внешних контуров подготовлен", description: "Здесь появятся форма отправки задачи в другой проект, список отправленных запросов и их статусные маркеры.", - request_title: "Отправка во внешний контур", - request_description: - "На следующем этапе здесь будет форма выбора целевого проекта, исполнителя, приоритета, срока и описания.", - list_title: "Отправленные запросы", - list_description: - "Здесь будет список межпроектных запросов с текущим статусом, ссылкой на целевую задачу и дальнейшей синхронизацией.", + detail_title: "Выберите запрос для просмотра деталей.", + open_title: "Нет открытых запросов", + open_description: "Здесь будут отображаться отправленные и активные межконтурные запросы.", + closed_title: "Нет закрытых запросов", + closed_description: "Здесь будут видны завершённые, принятые и отклонённые запросы.", + filtered_title: "Нет подходящих запросов", + filtered_description: "По текущим фильтрам запросы не найдены.", + }, + form: { + source_project: "Исходный внутренний контур", + target_project: "Целевой внешний контур", + selected_target: "Выбранный контур", + assignee: "Исполнитель", + due_date: "Срок", + }, + modal: { + title: "Добавить запрос", + submit: "Отправить", + toast_title: "Маршрутизация ещё не подключена", + toast_message: + "UI уже переведён на shell предложений. На следующем этапе подключим backend-маршрутизацию и реальную отправку в целевой контур.", + }, + properties: { + section_title: "Свойства", + target_contour: "Внешний контур", + target_contour_placeholder: "Будет доступно после подключения маршрутизации", + add_due_date: "Добавить срок выполнения", + duplicate_of: "Дубликат", + }, + actions: { + send: "Отправить", + accept: "Принять", + decline: "Отклонить", + copy: "Копировать ссылку", + open: "Открыть запрос", + unsupported_title: "Действие будет подключено следующим этапом", + unsupported_message: + "Кнопка «{action}» уже стоит на правильном месте в UI. Реальную маршрутизацию и обратный поток подключим следующим этапом.", }, }, deactivate_your_account: "Деактивировать ваш аккаунт", @@ -480,6 +526,7 @@ export default { no_matching_results: "Нет совпадений", title_is_required: "Требуется заголовок", title: "Заголовок", + properties: "Свойства", state: "Статус", priority: "Приоритет", none: "Нет", @@ -1257,6 +1304,7 @@ export default { snooze_permission: "Только администраторы проекта могут откладывать/возобновлять рабочие элементы", accept_permission: "Только администраторы проекта могут принимать рабочие элементы", decline_permission: "Только администраторы проекта могут отклонять рабочие элементы", + duplicate_permission: "Только администраторы проекта могут помечать рабочие элементы как дубликат", }, actions: { accept: "Принять", @@ -1271,14 +1319,15 @@ export default { }, source: { "in-app": "в приложении", + form_user: "Пользователь формы", }, order_by: { created_at: "Дата создания", updated_at: "Дата обновления", id: "ID", }, - label: "Входящие", - page_label: "{workspace} - Входящие", + label: "Предложения", + page_label: "{workspace} - Предложения", modal: { title: "Создать входящий рабочий элемент", },