From a4eaf4b247d208a742923c0dac4e2407494b135f Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sun, 19 Apr 2026 10:30:04 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=A3=D0=9D=D0=9A=D0=A6=D0=98=D0=98=20-?= =?UTF-8?q?=20=D0=9C=D0=95=D0=96=D0=9F=D0=A0=D0=9E=D0=95=D0=9A=D0=A2=D0=9D?= =?UTF-8?q?=D0=90=D0=AF=20=D0=9A=D0=9E=D0=9C=D0=9C=D0=A3=D0=9D=D0=98=D0=9A?= =?UTF-8?q?=D0=90=D0=A6=D0=98=D0=AF:=20source-side=20=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BE=D1=82=D0=BA=D1=80=D1=8B=D1=82=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=20=D0=B2=D0=BD=D0=B5=D1=88=D0=BD=D0=B5=D0=B3=D0=BE=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0=20=D0=B8=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B0=20=D0=BC?= =?UTF-8?q?=D0=B0=D1=80=D1=88=D1=80=D1=83=D1=82=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../phase-roadmap.md | 5 + .../api/plane/api/serializers/__init__.py | 1 + .../api/serializers/external_contours.py | 21 ++++ .../api/plane/api/urls/external_contours.py | 2 +- .../api/plane/api/views/external_contours.py | 50 ++++++++ .../api/plane/app/urls/external_contours.py | 2 +- .../api/plane/app/views/external_contours.py | 50 ++++++++ .../external-contours/content-root.tsx | 7 ++ .../external-contours/issue-properties.tsx | 75 +----------- .../projects/external-contours/issue-root.tsx | 111 ++++++++++++----- .../request-traceability.tsx | 113 +++++++++--------- .../external-contour.service.ts | 13 ++ .../project-external-contours.store.ts | 29 +++++ .../i18n/src/locales/en/translations.ts | 5 +- .../i18n/src/locales/ru/translations.ts | 5 +- 15 files changed, 318 insertions(+), 171 deletions(-) diff --git a/docs_prod/cross-project-task-routing/phase-roadmap.md b/docs_prod/cross-project-task-routing/phase-roadmap.md index 302aedb..fc8db4d 100644 --- a/docs_prod/cross-project-task-routing/phase-roadmap.md +++ b/docs_prod/cross-project-task-routing/phase-roadmap.md @@ -145,8 +145,13 @@ Что уже работает: - source-side detail использует отдельный экран `Внешних контуров` - в карточке отображается блок маршрутизации с ключевой source-target связью +- у отправителя открытый внешний запрос редактируется прямо из source-side карточки по полям `заголовок` и `описание`, даже без membership в target project - текущий статус берется из фактического state целевой задачи - если у инициатора нет membership в target project, карточка переключается в source-side readonly режим без прямого открытия чужого проекта +- блок `Маршрутизация` перестроен в фиксированный формат `3 x 3` +- в `Маршрутизацию` перенесены `Назначенный` и `Срок выполнения` +- в блоке `Свойства` убраны дубли `Внешний контур`, `Назначенный` и `Срок выполнения` +- название исходного внутреннего контура в карточке маршрутизации берется из живого проекта, а не из застывшего metadata snapshot - для закрытого внешнего запроса доступны source-side действия `Принять` и `Отклонить` - `Принять` фиксирует решение источника в bridge metadata и помечает запрос как принятый во внутренний контур - `Отклонить` требует комментарий причины, возвращает target issue в default-state целевого проекта и переносит source-side карточку обратно в список `Открытые` diff --git a/plane-src/apps/api/plane/api/serializers/__init__.py b/plane-src/apps/api/plane/api/serializers/__init__.py index 0908272..5a06ae2 100644 --- a/plane-src/apps/api/plane/api/serializers/__init__.py +++ b/plane-src/apps/api/plane/api/serializers/__init__.py @@ -57,6 +57,7 @@ from .external_contours import ( ExternalContourRequestCreateSerializer, ExternalContourRequestDecisionSerializer, ExternalContourRequestReplySerializer, + ExternalContourRequestUpdateSerializer, ExternalContourRequestSerializer, ExternalContourTargetOptionsSerializer, ExternalContourTargetProjectSerializer, diff --git a/plane-src/apps/api/plane/api/serializers/external_contours.py b/plane-src/apps/api/plane/api/serializers/external_contours.py index ff7eab7..f9ce935 100644 --- a/plane-src/apps/api/plane/api/serializers/external_contours.py +++ b/plane-src/apps/api/plane/api/serializers/external_contours.py @@ -41,6 +41,22 @@ class ExternalContourRequestReplySerializer(serializers.Serializer): comment = serializers.CharField() +class ExternalContourRequestUpdateSerializer(serializers.Serializer): + name = serializers.CharField(max_length=255, required=False) + description_html = serializers.CharField(required=False, allow_blank=True, allow_null=True) + + def validate(self, data): + if not data: + raise serializers.ValidationError("At least one field must be provided") + + if "name" in data: + data["name"] = data["name"].strip() + if not data["name"]: + raise serializers.ValidationError({"name": "Title is required"}) + + return data + + class ExternalContourTargetProjectSerializer(BaseSerializer): inbox_view = serializers.BooleanField(read_only=True, source="intake_view") @@ -257,6 +273,11 @@ class ExternalContourRequestSerializer(BaseSerializer): return ExternalContourMirroredCommentSerializer(comments, many=True).data def get_source_project_name(self, obj): + source_project_id = obj.extra.get("source_project_id") + if source_project_id: + live_name = Project.objects.filter(pk=source_project_id).values_list("name", flat=True).first() + if live_name: + return live_name return obj.extra.get("source_project_name") def get_source_decision(self, obj): diff --git a/plane-src/apps/api/plane/api/urls/external_contours.py b/plane-src/apps/api/plane/api/urls/external_contours.py index 653d7e0..a1148ff 100644 --- a/plane-src/apps/api/plane/api/urls/external_contours.py +++ b/plane-src/apps/api/plane/api/urls/external_contours.py @@ -33,7 +33,7 @@ urlpatterns = [ ), path( "workspaces//projects//external-contours//", - ExternalContourDetailEndpoint.as_view(http_method_names=["get"]), + ExternalContourDetailEndpoint.as_view(http_method_names=["get", "patch"]), name="external-contour-detail", ), path( diff --git a/plane-src/apps/api/plane/api/views/external_contours.py b/plane-src/apps/api/plane/api/views/external_contours.py index 1e985b1..9c5a9df 100644 --- a/plane-src/apps/api/plane/api/views/external_contours.py +++ b/plane-src/apps/api/plane/api/views/external_contours.py @@ -14,6 +14,7 @@ from plane.api.serializers import ( ExternalContourRequestCreateSerializer, ExternalContourRequestDecisionSerializer, ExternalContourRequestReplySerializer, + ExternalContourRequestUpdateSerializer, ExternalContourRequestSerializer, ExternalContourTargetOptionsSerializer, ExternalContourTargetProjectSerializer, @@ -304,6 +305,55 @@ class ExternalContourDetailEndpoint(BaseAPIView): ) return Response(serializer.data, status=status.HTTP_200_OK) + def patch(self, request, slug, project_id, request_id): + contour_request = get_object_or_404(self.get_queryset()) + issue = contour_request.issue + + if not issue: + return Response({"error": "Target issue was not found"}, status=status.HTTP_404_NOT_FOUND) + + requested_by_id = contour_request.extra.get("requested_by_id") or ( + str(issue.created_by_id) if issue.created_by_id else None + ) + if str(request.user.id) != str(requested_by_id): + return Response({"error": "Only the sender can edit this request"}, status=status.HTTP_403_FORBIDDEN) + + if issue.state and issue.state.group in [StateGroup.COMPLETED.value, StateGroup.CANCELLED.value]: + return Response( + {"error": "Only open external contour requests can be edited"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = ExternalContourRequestUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + issue_serializer = IssueCreateSerializer( + issue, + data=serializer.validated_data, + partial=True, + context={ + "project_id": str(issue.project_id), + "workspace_id": str(issue.workspace_id), + }, + ) + issue_serializer.is_valid(raise_exception=True) + issue_serializer.save() + + contour_request.updated_at = timezone.now() + contour_request.save(update_fields=["updated_at"]) + contour_request.refresh_from_db() + + response_serializer = ExternalContourRequestSerializer( + contour_request, + context={ + "include_mirror_data": True, + "workspace_slug": slug, + "source_project_id": str(project_id), + "request": request, + }, + ) + return Response(response_serializer.data, status=status.HTTP_200_OK) + class ExternalContourDecisionEndpoint(BaseAPIView): permission_classes = [ProjectLitePermission] diff --git a/plane-src/apps/api/plane/app/urls/external_contours.py b/plane-src/apps/api/plane/app/urls/external_contours.py index 653d7e0..a1148ff 100644 --- a/plane-src/apps/api/plane/app/urls/external_contours.py +++ b/plane-src/apps/api/plane/app/urls/external_contours.py @@ -33,7 +33,7 @@ urlpatterns = [ ), path( "workspaces//projects//external-contours//", - ExternalContourDetailEndpoint.as_view(http_method_names=["get"]), + ExternalContourDetailEndpoint.as_view(http_method_names=["get", "patch"]), name="external-contour-detail", ), path( diff --git a/plane-src/apps/api/plane/app/views/external_contours.py b/plane-src/apps/api/plane/app/views/external_contours.py index 1e985b1..9c5a9df 100644 --- a/plane-src/apps/api/plane/app/views/external_contours.py +++ b/plane-src/apps/api/plane/app/views/external_contours.py @@ -14,6 +14,7 @@ from plane.api.serializers import ( ExternalContourRequestCreateSerializer, ExternalContourRequestDecisionSerializer, ExternalContourRequestReplySerializer, + ExternalContourRequestUpdateSerializer, ExternalContourRequestSerializer, ExternalContourTargetOptionsSerializer, ExternalContourTargetProjectSerializer, @@ -304,6 +305,55 @@ class ExternalContourDetailEndpoint(BaseAPIView): ) return Response(serializer.data, status=status.HTTP_200_OK) + def patch(self, request, slug, project_id, request_id): + contour_request = get_object_or_404(self.get_queryset()) + issue = contour_request.issue + + if not issue: + return Response({"error": "Target issue was not found"}, status=status.HTTP_404_NOT_FOUND) + + requested_by_id = contour_request.extra.get("requested_by_id") or ( + str(issue.created_by_id) if issue.created_by_id else None + ) + if str(request.user.id) != str(requested_by_id): + return Response({"error": "Only the sender can edit this request"}, status=status.HTTP_403_FORBIDDEN) + + if issue.state and issue.state.group in [StateGroup.COMPLETED.value, StateGroup.CANCELLED.value]: + return Response( + {"error": "Only open external contour requests can be edited"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = ExternalContourRequestUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + issue_serializer = IssueCreateSerializer( + issue, + data=serializer.validated_data, + partial=True, + context={ + "project_id": str(issue.project_id), + "workspace_id": str(issue.workspace_id), + }, + ) + issue_serializer.is_valid(raise_exception=True) + issue_serializer.save() + + contour_request.updated_at = timezone.now() + contour_request.save(update_fields=["updated_at"]) + contour_request.refresh_from_db() + + response_serializer = ExternalContourRequestSerializer( + contour_request, + context={ + "include_mirror_data": True, + "workspace_slug": slug, + "source_project_id": str(project_id), + "request": request, + }, + ) + return Response(response_serializer.data, status=status.HTTP_200_OK) + class ExternalContourDecisionEndpoint(BaseAPIView): permission_classes = [ProjectLitePermission] 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 index 4dbaa21..22700c5 100644 --- 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 @@ -63,6 +63,12 @@ export const ExternalContoursContentRoot = observer(function ExternalContoursCon hasDirectTargetAccess && (allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, targetProjectId) || issue?.created_by === currentUser?.id); + const sourceRequesterId = contourRequest?.requested_by_id || issue?.created_by || contourRequest?.created_by; + const isSourceEditable = + !hasDirectTargetAccess && + contourRequest?.status === "open" && + !!currentUser?.id && + String(sourceRequesterId) === String(currentUser.id); const isGuest = !!targetProjectId && getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, targetProjectId) === EUserPermissions.GUEST; @@ -91,6 +97,7 @@ export const ExternalContoursContentRoot = observer(function ExternalContoursCon contourRequest={contourRequest} hasDirectTargetAccess={hasDirectTargetAccess} isEditable={!!isEditable && !readOnly} + isSourceEditable={isSourceEditable} isSubmitting={isSubmitting} setIsSubmitting={setIsSubmitting} /> 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 index c2c33b0..a01881c 100644 --- 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 @@ -6,18 +6,8 @@ import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; -import { - DueDatePropertyIcon, - LabelPropertyIcon, - MembersPropertyIcon, - PriorityPropertyIcon, - StatePropertyIcon, -} from "@plane/propel/icons"; +import { LabelPropertyIcon, PriorityPropertyIcon } from "@plane/propel/icons"; 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"; @@ -36,8 +26,6 @@ export const ExternalContoursIssueContentProperties = observer(function External const { workspaceSlug, targetProjectId, issue, issueOperations, isEditable } = props; const { t } = useTranslation(); - const minDate = issue.start_date ? getDate(issue.start_date) : null; - minDate?.setDate(minDate.getDate()); if (!issue || !issue?.id) return <>; return ( @@ -46,38 +34,6 @@ export const ExternalContoursIssueContentProperties = observer(function External
{t("external_contours_page.properties.section_title")}
-
-
- - {t("external_contours_page.properties.target_contour")} -
-
- {issue.project_detail?.name || t("common.none")} -
-
- -
-
- - {t("assignees")} -
- issue?.id && issueOperations.update(workspaceSlug, targetProjectId, issue.id, { assignee_ids: val })} - disabled={!isEditable} - projectId={targetProjectId} - 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" - /> -
-
@@ -93,35 +49,6 @@ export const ExternalContoursIssueContentProperties = observer(function External buttonClassName="h-auto w-min whitespace-nowrap" />
-
-
- -
-
-
-
- - {t("due_date")} -
- - issue?.id && - issueOperations.update(workspaceSlug, targetProjectId, 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" - /> -
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 index f7b49bc..d092c44 100644 --- 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 @@ -13,7 +13,7 @@ import { Badge } from "@plane/propel/badge"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { TExternalContourRequest, TIssue, TNameDescriptionLoader } from "@plane/types"; import { EFileAssetType } from "@plane/types"; -import { getTextContent, renderFormattedDate } from "@plane/utils"; +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"; @@ -45,16 +45,26 @@ type Props = { contourRequest: TExternalContourRequest; hasDirectTargetAccess: boolean; isEditable: boolean; + isSourceEditable: boolean; isSubmitting: TNameDescriptionLoader; setIsSubmitting: Dispatch>; }; export const ExternalContoursIssueMainContent = observer(function ExternalContoursIssueMainContent(props: Props) { - const { workspaceSlug, sourceProjectId, contourRequest, hasDirectTargetAccess, isEditable, isSubmitting, setIsSubmitting } = props; + const { + workspaceSlug, + sourceProjectId, + contourRequest, + hasDirectTargetAccess, + isEditable, + isSourceEditable, + isSubmitting, + setIsSubmitting, + } = props; const { t } = useTranslation(); const editorRef = useRef(null); const { data: currentUser } = useUser(); - const { loader, updateRequestIssue } = useProjectExternalContours(); + const { loader, updateRequest, updateRequestIssue } = useProjectExternalContours(); const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting"); useEffect(() => { @@ -107,17 +117,77 @@ export const ExternalContoursIssueMainContent = observer(function ExternalContou [contourRequest.id, targetProjectId, t, updateRequestIssue, workspaceSlug] ); + const sourceIssueOperations: TIssueOperations = useMemo( + () => ({ + fetch: async () => undefined, + remove: async () => undefined, + update: async (_workspaceSlug: string, _projectId: string, requestId: string, data: Partial) => { + try { + await updateRequest(workspaceSlug, sourceProjectId, requestId, { + name: data.name, + description_html: data.description_html, + }); + } catch { + setToast({ title: t("error"), type: TOAST_TYPE.ERROR, message: t("issue_could_not_be_updated") }); + } + }, + }), + [sourceProjectId, t, updateRequest, workspaceSlug] + ); + if (!issue || !issue.project_id || !issue.id) return <>; if (!hasDirectTargetAccess) { return ( <>
-

{issue.name}

-

" }} - /> + {isSourceEditable ? ( + <> + setIsSubmitting(value)} + issueOperations={sourceIssueOperations} + disabled={false} + value={issue.name} + containerClassName="-ml-3" + /> + + {loader === "issue-loading" || issue.description_html === undefined ? ( + + ) : ( +

" : issue.description_html} + key={`${issue.id}-source`} + onSubmit={async (value) => { + await sourceIssueOperations.update(workspaceSlug, sourceProjectId, contourRequest.id, { + description_html: value.description_html, + }); + }} + projectId={sourceProjectId} + setIsSubmitting={(value) => setIsSubmitting(value)} + workspaceSlug={workspaceSlug} + /> + )} + + ) : ( + <> +

{issue.name}

+

" }} + /> + + )}
@@ -127,34 +197,11 @@ export const ExternalContoursIssueMainContent = observer(function ExternalContou
{t("external_contours_page.properties.section_title")}
-
- {t("external_contours_page.properties.target_contour")} - {issue.project_detail?.name || t("common.none")} -
- -
- {t("assignees")} - {issue.assignee_details?.length ? ( - issue.assignee_details.map((assignee) => ( - - {assignee.display_name} - - )) - ) : ( - {t("external_contours_page.list.unassigned")} - )} -
-
{t("priority")} {issue.priority || t("none")}
-
- {t("due_date")} - {issue.target_date ? renderFormattedDate(issue.target_date) : t("common.none")} -
-
{t("labels")} {issue.label_details?.length ? ( @@ -183,7 +230,7 @@ export const ExternalContoursIssueMainContent = observer(function ExternalContou -
{t("external_contours_page.readonly_source_view")}
+ {!isSourceEditable &&
{t("external_contours_page.readonly_source_view")}
} ); } diff --git a/plane-src/apps/web/ce/components/projects/external-contours/request-traceability.tsx b/plane-src/apps/web/ce/components/projects/external-contours/request-traceability.tsx index e57d964..8e39fc1 100644 --- a/plane-src/apps/web/ce/components/projects/external-contours/request-traceability.tsx +++ b/plane-src/apps/web/ce/components/projects/external-contours/request-traceability.tsx @@ -4,6 +4,7 @@ * See the LICENSE file for details. */ +import type { ReactNode } from "react"; import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; import { Badge } from "@plane/propel/badge"; @@ -16,6 +17,13 @@ type Props = { contourRequest: TExternalContourRequest; }; +const TraceabilityCell = ({ label, children }: { label: string; children: ReactNode }) => ( +
+
{label}
+
{children}
+
+); + export const ExternalContoursRequestTraceability = observer(function ExternalContoursRequestTraceability(props: Props) { const { contourRequest } = props; const { t } = useTranslation(); @@ -25,16 +33,13 @@ export const ExternalContoursRequestTraceability = observer(function ExternalCon const requestedAt = contourRequest.requested_at || contourRequest.created_at; const lastUpdatedAt = issue.updated_at || contourRequest.updated_at; const targetProjectName = contourRequest.target_project_name || issue.project_detail?.name || t("common.none"); + const sourceProjectName = contourRequest.source_project_name || t("common.none"); const sourceDecision = contourRequest.source_decision === "accepted" ? t("external_contours_page.traceability.source_decision_accepted") : t("external_contours_page.traceability.source_decision_pending"); - const targetIssueKey = - issue.project_detail?.identifier && issue.sequence_id - ? `${issue.project_detail.identifier}-${issue.sequence_id}` - : issue.sequence_id - ? `#${issue.sequence_id}` - : t("common.none"); + const dueDate = issue.target_date ? renderFormattedDate(issue.target_date) : t("common.none"); + const assigneeDetails = issue.assignee_details ?? []; return (
@@ -43,66 +48,56 @@ export const ExternalContoursRequestTraceability = observer(function ExternalCon

{t("external_contours_page.traceability.description")}

-
-
-
{t("external_contours_page.traceability.source_contour")}
-
- {contourRequest.source_project_name || t("common.none")} -
-
+
+ + {sourceProjectName} + -
-
{t("external_contours_page.traceability.target_contour")}
-
- {targetProjectName} -
-
+ + {targetProjectName} + -
-
{t("external_contours_page.traceability.status")}
-
- -
-
+ + + -
-
{t("external_contours_page.traceability.source_decision")}
-
{sourceDecision}
-
- -
-
{t("external_contours_page.traceability.requested_by")}
-
- - {requestedByName} + +
+ + {requestedByName}
-
+ -
-
{t("external_contours_page.traceability.requested_at")}
-
- {requestedAt ? renderFormattedDate(requestedAt) : t("common.none")} -
-
+ + {assigneeDetails.length > 0 ? ( +
+ {assigneeDetails.map((assignee) => ( +
+ + {assignee.display_name} +
+ ))} +
+ ) : ( + t("external_contours_page.list.unassigned") + )} +
-
-
{t("external_contours_page.traceability.last_updated")}
-
- {lastUpdatedAt ? renderFormattedDate(lastUpdatedAt) : t("common.none")} -
-
+ + {sourceDecision} + -
-
{t("external_contours_page.traceability.linked_item")}
-
- {targetIssueKey} -
-
+ + {requestedAt ? renderFormattedDate(requestedAt) : t("common.none")} + + + + {dueDate} + + + + {lastUpdatedAt ? renderFormattedDate(lastUpdatedAt) : t("common.none")} +
); diff --git a/plane-src/apps/web/core/services/external-contours/external-contour.service.ts b/plane-src/apps/web/core/services/external-contours/external-contour.service.ts index 5875156..eee31c9 100644 --- a/plane-src/apps/web/core/services/external-contours/external-contour.service.ts +++ b/plane-src/apps/web/core/services/external-contours/external-contour.service.ts @@ -35,6 +35,19 @@ export class ExternalContourService extends APIService { }); } + async updateRequest( + workspaceSlug: string, + projectId: string, + requestId: string, + data: Pick, "name" | "description_html"> + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/external-contours/${requestId}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async listTargetProjects(workspaceSlug: string, projectId: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/external-contours/targets/`) .then((response) => response?.data) diff --git a/plane-src/apps/web/core/store/external-contours/project-external-contours.store.ts b/plane-src/apps/web/core/store/external-contours/project-external-contours.store.ts index 91a3447..c53b624 100644 --- a/plane-src/apps/web/core/store/external-contours/project-external-contours.store.ts +++ b/plane-src/apps/web/core/store/external-contours/project-external-contours.store.ts @@ -34,6 +34,12 @@ export interface IProjectExternalContoursStore { projectId: string, data: Partial & { target_project_id?: string | null } ) => Promise; + updateRequest: ( + workspaceSlug: string, + projectId: string, + requestId: string, + data: Pick, "name" | "description_html"> + ) => Promise; decideRequest: ( workspaceSlug: string, projectId: string, @@ -95,6 +101,7 @@ export class ProjectExternalContoursStore implements IProjectExternalContoursSto fetchRequests: action, fetchRequestById: action, createRequest: action, + updateRequest: action, decideRequest: action, replyToRequest: action, handleCurrentTab: action, @@ -235,6 +242,28 @@ export class ProjectExternalContoursStore implements IProjectExternalContoursSto } }; + updateRequest = async ( + workspaceSlug: string, + projectId: string, + requestId: string, + data: Pick, "name" | "description_html"> + ) => { + this.loader = "mutation-loading"; + try { + const request = await this.externalContourService.updateRequest(workspaceSlug, projectId, requestId, data); + runInAction(() => { + this.upsertRequests([request]); + this.loader = undefined; + }); + return request; + } catch (error) { + runInAction(() => { + this.loader = undefined; + }); + throw error; + } + }; + decideRequest = async ( workspaceSlug: string, projectId: string, diff --git a/plane-src/packages/i18n/src/locales/en/translations.ts b/plane-src/packages/i18n/src/locales/en/translations.ts index 82bf78a..37d3e76 100644 --- a/plane-src/packages/i18n/src/locales/en/translations.ts +++ b/plane-src/packages/i18n/src/locales/en/translations.ts @@ -365,7 +365,7 @@ export default { traceability: { title: "Routing", description: - "This block shows which contour sent the request, where it was routed, and which linked work item now carries the execution.", + "This block shows which contour sent the request, where it was routed, and what state the work is currently in.", source_contour: "Source internal contour", source_decision: "Source-side decision", source_decision_pending: "Awaiting decision", @@ -373,9 +373,10 @@ export default { target_contour: "Target external contour", status: "Current status", requested_by: "Requested by", + assignee: "Assignee", requested_at: "Sent at", + due_date: "Due date", last_updated: "Last updated", - linked_item: "Linked work item", }, actions: { send: "Send", diff --git a/plane-src/packages/i18n/src/locales/ru/translations.ts b/plane-src/packages/i18n/src/locales/ru/translations.ts index 107a104..a56fe7f 100644 --- a/plane-src/packages/i18n/src/locales/ru/translations.ts +++ b/plane-src/packages/i18n/src/locales/ru/translations.ts @@ -521,7 +521,7 @@ export default { }, traceability: { title: "Маршрутизация", - description: "Здесь отображается, из какого контура ушёл запрос, куда он направлен и с каким связанным элементом он сейчас работает.", + description: "Здесь отображается, из какого контура ушёл запрос, куда он направлен и в каком состоянии находится работа по нему.", source_contour: "Исходный внутренний контур", source_decision: "Решение источника", source_decision_pending: "Ожидает решения", @@ -529,9 +529,10 @@ export default { target_contour: "Целевой внешний контур", status: "Текущий статус", requested_by: "Отправитель", + assignee: "Назначенный", requested_at: "Отправлено", + due_date: "Срок выполнения", last_updated: "Последнее изменение", - linked_item: "Связанная задача", }, actions: { send: "Отправить",