UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: фикс popup рабочего пространства, sidebar-search и полировка модалки внешнего запроса

This commit is contained in:
DCCONSTRUCTIONS 2026-04-19 21:02:35 +03:00
parent 01eb3d4c8a
commit 21581373cd
11 changed files with 278 additions and 147 deletions

View File

@ -22,7 +22,7 @@ export function ExternalContourCreateModalRoot(props: Props) {
isOpen={modalState}
position={EModalPosition.CENTER}
width={EModalWidth.XXXXL}
className="rounded-lg !bg-transparent shadow-none transition-[width] ease-linear"
className="transition-[width] ease-linear"
>
<ExternalContoursCreateRoot workspaceSlug={workspaceSlug} projectId={projectId} handleModalClose={handleModalClose} />
</ModalCore>

View File

@ -4,13 +4,13 @@
* See the LICENSE file for details.
*/
import { useMemo } from "react";
import { CalendarDays, SignalHigh } from "lucide-react";
import { observer } from "mobx-react";
import { ISSUE_PRIORITIES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Badge } from "@plane/propel/badge";
import { MembersPropertyIcon } from "@plane/propel/icons";
import { LabelPropertyIcon, MembersPropertyIcon, PriorityIcon, ProjectIcon } from "@plane/propel/icons";
import type { TIssue, TIssuePriorities } from "@plane/types";
import { renderFormattedPayloadDate } from "@plane/utils";
import { cn, renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils";
import { DateDropdown } from "@/components/dropdowns/date";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { MemberDropdownBase } from "@/components/dropdowns/member/base";
@ -31,30 +31,31 @@ export const ExternalContoursCreateProperties = observer(function ExternalContou
const { t } = useTranslation();
const { getUserDetails } = useMember();
const { targetProjectIds, getTargetOptionsByProjectId, getTargetProjectById } = useProjectExternalContours();
const controlClassName =
"nodedc-modal-field flex h-10 min-w-fit items-center gap-2 rounded-[1.25rem] px-3.5 text-[12px] font-medium text-secondary";
const selectedTargetProject = data.target_project_id ? getTargetProjectById(data.target_project_id) : undefined;
const selectedTargetOptions = getTargetOptionsByProjectId(data.target_project_id);
const targetLabelIds = useMemo(
() => selectedTargetOptions?.labels?.map((label) => label.id) ?? [],
[selectedTargetOptions?.labels]
);
const assigneeLabel = useMemo(() => {
const assigneeIds = data.assignee_ids || [];
if (!assigneeIds.length) return t("external_contours_page.form.assignee");
if (assigneeIds.length === 1) return getUserDetails(assigneeIds[0])?.display_name || t("external_contours_page.form.assignee");
return `${assigneeIds.length} ${t("assignees").toLocaleLowerCase()}`;
}, [data.assignee_ids, getUserDetails, t]);
const getTargetLabelById = (labelId: string) =>
selectedTargetOptions?.labels?.find((label) => label.id === labelId) ?? null;
const targetLabelIds = selectedTargetOptions?.labels?.map((label) => label.id) ?? [];
const selectedLabels = (data.label_ids || []).map((labelId) => getTargetLabelById(labelId)).filter((label) => !!label);
const assigneeIds = data.assignee_ids || [];
const assigneeLabel = !assigneeIds.length
? t("external_contours_page.form.assignee")
: assigneeIds.length === 1
? (getUserDetails(assigneeIds[0])?.display_name ?? t("external_contours_page.form.assignee"))
: `${assigneeIds.length} ${t("assignees").toLocaleLowerCase()}`;
const priorityDetails = ISSUE_PRIORITIES.find((priority) => priority.key === data.priority);
return (
<div className="relative flex flex-wrap items-center gap-2">
<div className="rounded-md border border-subtle bg-surface-2 px-3 py-1.5 text-11 text-secondary">
<span className="mr-2 text-tertiary">{t("external_contours_page.form.source_project")}</span>
<Badge variant="neutral">{currentProjectName || "NODE.DC"}</Badge>
<div className="relative flex flex-wrap items-center gap-2.5">
<div className={cn(controlClassName, "min-w-[16rem] justify-start")}>
<span className="text-tertiary">{t("external_contours_page.form.source_project")}</span>
<span className="truncate text-primary">{currentProjectName || "NODE.DC"}</span>
</div>
<div className="h-7">
<div className="h-10">
<ProjectDropdownBase
value={data.target_project_id ?? null}
onChange={(value) => {
@ -67,41 +68,36 @@ export const ExternalContoursCreateProperties = observer(function ExternalContou
multiple={false}
projectIds={targetProjectIds}
getProjectById={getTargetProjectById}
buttonVariant="border-with-text"
buttonVariant="transparent-without-text"
buttonContainerClassName="h-full"
button={
<div className={cn(controlClassName, "min-w-[16rem] justify-start")}>
<ProjectIcon className="h-3.5 w-3.5 flex-shrink-0 text-tertiary" />
<span className={cn("truncate", selectedTargetProject ? "text-primary" : "text-tertiary")}>
{selectedTargetProject?.name ?? t("external_contours_page.form.target_project")}
</span>
</div>
}
placeholder={t("external_contours_page.form.target_project")}
disabled={targetProjectIds.length === 0}
/>
</div>
{selectedTargetProject && (
<div className="rounded-md border border-subtle bg-surface-2 px-3 py-1.5 text-11 text-secondary">
<span className="mr-2 text-tertiary">{t("external_contours_page.form.selected_target")}</span>
<Badge variant="neutral">{selectedTargetProject.name}</Badge>
</div>
)}
<div className="h-7">
<PriorityDropdown
value={data.priority}
onChange={(priority) => handleData("priority", priority)}
buttonVariant="border-with-text"
/>
</div>
<div className="h-7">
<div className="h-10">
<MemberDropdownBase
value={data.assignee_ids || []}
onChange={(assigneeIds) => handleData("assignee_ids", assigneeIds)}
value={assigneeIds}
onChange={(nextAssigneeIds) => handleData("assignee_ids", nextAssigneeIds)}
getUserDetails={getUserDetails}
memberIds={selectedTargetOptions?.member_ids ?? []}
button={
<div className="flex h-full items-center justify-start gap-1.5 rounded-sm border-[0.5px] border-strong px-1.5 text-11 text-secondary">
<ButtonAvatars showTooltip={false} userIds={data.assignee_ids || []} icon={MembersPropertyIcon} />
<span className="flex-grow truncate text-left text-body-xs-medium leading-5">{assigneeLabel}</span>
<div className={cn(controlClassName, "min-w-[12rem] justify-start")}>
<ButtonAvatars showTooltip={false} userIds={assigneeIds} icon={MembersPropertyIcon} />
<span className={cn("truncate", assigneeIds.length > 0 ? "text-primary" : "text-tertiary")}>
{assigneeLabel}
</span>
</div>
}
buttonVariant={(data.assignee_ids || []).length > 0 ? "transparent-without-text" : "border-with-text"}
buttonClassName={(data.assignee_ids || []).length > 0 ? "hover:bg-transparent" : ""}
buttonVariant="transparent-without-text"
optionsClassName="z-[60]"
placeholder={t("external_contours_page.form.assignee")}
disabled={!data.target_project_id || !selectedTargetOptions}
@ -109,21 +105,67 @@ export const ExternalContoursCreateProperties = observer(function ExternalContou
/>
</div>
<div className="h-7">
<div className="h-10">
<PriorityDropdown
value={data.priority}
onChange={(priority) => handleData("priority", priority)}
buttonVariant="transparent-without-text"
buttonContainerClassName="h-full"
button={
<div className={cn(controlClassName, "min-w-[12rem] justify-start")}>
{data.priority && data.priority !== "none" ? (
<PriorityIcon priority={data.priority} size={14} />
) : (
<SignalHigh className="h-3.5 w-3.5 flex-shrink-0 text-tertiary" />
)}
<span className={cn("truncate", data.priority && data.priority !== "none" ? "text-primary" : "text-tertiary")}>
{data.priority && data.priority !== "none"
? priorityDetails?.title
: t("external_contours_page.form.priority")}
</span>
</div>
}
placeholder={t("external_contours_page.form.priority")}
/>
</div>
<div className="h-10">
<WorkItemLabelSelectBase
value={data.label_ids || []}
onChange={(labelIds) => handleData("label_ids", labelIds)}
getLabelById={getTargetLabelById}
labelIds={targetLabelIds}
buttonContainerClassName="h-full !text-[12px]"
label={
<div className={cn(controlClassName, "min-w-[9rem] justify-start !text-[12px]")}>
<LabelPropertyIcon className="h-3.5 w-3.5 flex-shrink-0 text-tertiary" />
<span className={cn("truncate", selectedLabels.length > 0 ? "text-primary" : "text-tertiary")}>
{selectedLabels.length > 0
? selectedLabels.length === 1
? selectedLabels[0]?.name
: `${selectedLabels.length} ${t("labels").toLocaleLowerCase()}`
: t("labels")}
</span>
</div>
}
disabled={!data.target_project_id || !selectedTargetOptions}
/>
</div>
<div className="h-7">
<div className="h-10">
<DateDropdown
value={data.target_date || null}
onChange={(date) => handleData("target_date", date ? renderFormattedPayloadDate(date) : "")}
buttonVariant="border-with-text"
buttonVariant="transparent-without-text"
buttonContainerClassName="h-full"
button={
<div className={cn(controlClassName, "min-w-[10rem] justify-start")}>
<CalendarDays className="h-3.5 w-3.5 flex-shrink-0 text-tertiary" />
<span className={cn("truncate", data.target_date ? "text-primary" : "text-tertiary")}>
{data.target_date ? renderFormattedDate(data.target_date) : t("external_contours_page.form.due_date")}
</span>
</div>
}
placeholder={t("external_contours_page.form.due_date")}
/>
</div>

View File

@ -126,57 +126,67 @@ export const ExternalContoursCreateRoot = observer(function ExternalContoursCrea
if (!workspaceSlug || !projectId || !workspaceId) return <></>;
return (
<div className="flex w-full gap-2 bg-transparent">
<div className="w-full rounded-lg">
<form onSubmit={handleFormSubmit} className="flex w-full flex-col">
<div className="space-y-5 rounded-t-lg bg-surface-1 p-5">
<div className="flex items-center justify-between gap-2">
<h3 className="text-18 font-medium text-secondary">{t("external_contours_page.modal.title")}</h3>
</div>
<div className="space-y-3">
<InboxIssueTitle
data={formData}
handleData={handleFormData as any}
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
/>
<InboxIssueDescription
workspaceSlug={workspaceSlug}
projectId={projectId}
workspaceId={workspaceId}
data={formData}
handleData={handleFormData as any}
editorRef={descriptionEditorRef}
containerClassName="min-h-[150px] border-[0.5px] border-subtle-1 bg-layer-2 py-3"
onAssetUpload={() => {}}
/>
<ExternalContoursCreateProperties
currentProjectName={currentProjectDetails?.name}
data={formData}
handleData={handleFormData as any}
/>
</div>
</div>
<div className="flex items-center justify-between gap-2 rounded-b-lg border-t-[0.5px] border-subtle bg-surface-1 px-5 py-4">
<div
className="inline-flex cursor-pointer items-center gap-1.5"
onClick={() => setCreateMore((prevData) => !prevData)}
role="button"
tabIndex={0}
>
<ToggleSwitch value={createMore} onChange={() => {}} size="sm" />
<span className="text-11">{t("create_more")}</span>
</div>
<div className="flex items-center gap-3">
<Button variant="secondary" size="lg" type="button" onClick={handleModalClose}>
{t("cancel")}
</Button>
<Button type="submit" variant="primary" size="lg" loading={formSubmitting} disabled={!canSubmit || isTitleLengthMoreThan255Character}>
{t("external_contours_page.modal.submit")}
</Button>
</div>
</div>
</form>
<form onSubmit={handleFormSubmit} className="flex w-full flex-col gap-6 px-6 py-6">
<div className="space-y-5">
<div className="flex items-center justify-between gap-2">
<h3 className="text-18 font-medium text-secondary">{t("external_contours_page.modal.title")}</h3>
</div>
<div className="space-y-4">
<InboxIssueTitle
data={formData}
handleData={handleFormData as any}
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
inputClassName="nodedc-modal-input !px-4 !py-3 !text-[15px]"
/>
<InboxIssueDescription
workspaceSlug={workspaceSlug}
projectId={projectId}
workspaceId={workspaceId}
data={formData}
handleData={handleFormData as any}
editorRef={descriptionEditorRef}
containerClassName="nodedc-modal-editor min-h-[180px] !border-none !bg-transparent !p-0"
onAssetUpload={() => {}}
/>
<ExternalContoursCreateProperties
currentProjectName={currentProjectDetails?.name}
data={formData}
handleData={handleFormData as any}
/>
</div>
</div>
</div>
<div className="flex items-center justify-between gap-3 pt-1">
<div
className="inline-flex cursor-pointer items-center gap-1.5"
onClick={() => setCreateMore((prevData) => !prevData)}
role="button"
tabIndex={0}
>
<ToggleSwitch value={createMore} onChange={() => {}} size="sm" />
<span className="text-11 text-secondary">{t("create_more")}</span>
</div>
<div className="flex items-center gap-3">
<Button
variant="secondary"
size="lg"
type="button"
onClick={handleModalClose}
className="min-w-[8.25rem] !rounded-[1.25rem] !border-transparent px-5 shadow-none"
>
{t("cancel")}
</Button>
<Button
type="submit"
variant="primary"
size="lg"
loading={formSubmitting}
disabled={!canSubmit || isTitleLengthMoreThan255Character}
className="min-w-[8.25rem] !rounded-[1.25rem] !border-transparent px-5 shadow-none"
>
{t("external_contours_page.modal.submit")}
</Button>
</div>
</div>
</form>
);
});

View File

@ -11,7 +11,7 @@ import { useTranslation } from "@plane/i18n";
import type { TIssue } from "@plane/types";
import { Input } from "@plane/ui";
// helpers
import { getTabIndex } from "@plane/utils";
import { cn, getTabIndex } from "@plane/utils";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
@ -19,10 +19,11 @@ type TInboxIssueTitle = {
data: Partial<TIssue>;
handleData: (issueKey: keyof Partial<TIssue>, issueValue: Partial<TIssue>[keyof Partial<TIssue>]) => void;
isTitleLengthMoreThan255Character?: boolean;
inputClassName?: string;
};
export const InboxIssueTitle = observer(function InboxIssueTitle(props: TInboxIssueTitle) {
const { data, handleData, isTitleLengthMoreThan255Character } = props;
const { data, handleData, isTitleLengthMoreThan255Character, inputClassName } = props;
// hooks
const { isMobile } = usePlatformOS();
@ -37,7 +38,7 @@ export const InboxIssueTitle = observer(function InboxIssueTitle(props: TInboxIs
value={data?.name}
onChange={(e) => handleData("name", e.target.value)}
placeholder={t("title")}
className="w-full text-14"
className={cn("w-full text-14", inputClassName)}
tabIndex={getIndex("name")}
required
/>

View File

@ -130,7 +130,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
const width = 320;
const viewportPadding = 16;
const left = Math.min(rect.left, window.innerWidth - width - viewportPadding);
const top = rect.top;
const top = rect.top + rect.height / 2;
setSidebarSearchPosition({
left,
@ -372,6 +372,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
left: `${sidebarSearchPosition.left}px`,
top: `${sidebarSearchPosition.top}px`,
width: `${sidebarSearchPosition.width}px`,
transform: "translateY(-50%)",
}}
>
<div className="relative">

View File

@ -38,7 +38,7 @@ export function ShortcutsModal(props: Props) {
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-30" onClose={handleClose}>
<Transition.Child
as={Fragment}
as="div"
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
@ -52,7 +52,7 @@ export function ShortcutsModal(props: Props) {
<div className="fixed inset-0 z-30 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
<Transition.Child
as={Fragment}
as="div"
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"

View File

@ -121,7 +121,7 @@ export const ProjectsAppPowerKModalWrapper = observer(function ProjectsAppPowerK
<Dialog as="div" className="relative z-50" onClose={onClose}>
{/* Backdrop */}
<Transition.Child
as={React.Fragment}
as="div"
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
@ -135,7 +135,7 @@ export const ProjectsAppPowerKModalWrapper = observer(function ProjectsAppPowerK
<div className="fixed inset-0 z-30 overflow-y-auto">
<div className="flex items-center justify-center p-4 sm:p-6 md:p-20">
<Transition.Child
as={React.Fragment}
as="div"
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"

View File

@ -4,7 +4,7 @@
* See the LICENSE file for details.
*/
import { Fragment, useState, useEffect, useCallback, useLayoutEffect, useRef } from "react";
import { Fragment, useEffect, useCallback, useLayoutEffect, useRef, useState, type RefObject } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { createPortal } from "react-dom";
@ -34,6 +34,56 @@ type WorkspaceMenuRootProps = {
variant: "sidebar" | "top-navigation" | "sidebar-panel";
};
type WorkspaceMenuStateSyncProps = {
open: boolean;
variant: WorkspaceMenuRootProps["variant"];
sidebarPanelButtonRef: RefObject<HTMLButtonElement | null>;
onSidebarDropdownToggle: (value: boolean) => void;
onSidebarPanelPositionChange: (position: { left: number; top: number; width: number } | null) => void;
};
function WorkspaceMenuStateSync(props: WorkspaceMenuStateSyncProps) {
const { open, variant, sidebarPanelButtonRef, onSidebarDropdownToggle, onSidebarPanelPositionChange } = props;
const updateSidebarPanelMenuPosition = useCallback(() => {
if (variant !== "sidebar-panel" || !sidebarPanelButtonRef.current || typeof window === "undefined") return;
const rect = sidebarPanelButtonRef.current.getBoundingClientRect();
const width = 480;
const viewportPadding = 16;
onSidebarPanelPositionChange({
left: Math.min(rect.left, window.innerWidth - width - viewportPadding),
top: rect.bottom + 8,
width,
});
}, [onSidebarPanelPositionChange, sidebarPanelButtonRef, variant]);
useEffect(() => {
onSidebarDropdownToggle(open);
}, [onSidebarDropdownToggle, open]);
useLayoutEffect(() => {
if (!open || variant !== "sidebar-panel") {
onSidebarPanelPositionChange(null);
return;
}
updateSidebarPanelMenuPosition();
const handlePositionUpdate = () => updateSidebarPanelMenuPosition();
window.addEventListener("resize", handlePositionUpdate);
window.addEventListener("scroll", handlePositionUpdate, true);
return () => {
window.removeEventListener("resize", handlePositionUpdate);
window.removeEventListener("scroll", handlePositionUpdate, true);
};
}, [onSidebarPanelPositionChange, open, updateSidebarPanelMenuPosition, variant]);
return null;
}
export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: WorkspaceMenuRootProps) {
const { variant } = props;
// store hooks
@ -48,7 +98,6 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
// translation
const { t } = useTranslation();
// local state
const [isWorkspaceMenuOpen, setIsWorkspaceMenuOpen] = useState(false);
const [sidebarPanelMenuPosition, setSidebarPanelMenuPosition] = useState<{
left: number;
top: number;
@ -77,40 +126,6 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
const workspacesList = orderWorkspacesList(Object.values(workspaces ?? {}));
// TODO: fix workspaces list scroll
const updateSidebarPanelMenuPosition = useCallback(() => {
if (variant !== "sidebar-panel" || !sidebarPanelButtonRef.current || typeof window === "undefined") return;
const rect = sidebarPanelButtonRef.current.getBoundingClientRect();
const width = 480;
const viewportPadding = 16;
setSidebarPanelMenuPosition({
left: Math.min(rect.left, window.innerWidth - width - viewportPadding),
top: rect.bottom + 8,
width,
});
}, [variant]);
// Toggle sidebar dropdown state when either menu is open
useEffect(() => {
toggleAnySidebarDropdown(isWorkspaceMenuOpen);
}, [isWorkspaceMenuOpen, toggleAnySidebarDropdown]);
useLayoutEffect(() => {
if (!isWorkspaceMenuOpen || variant !== "sidebar-panel") return;
updateSidebarPanelMenuPosition();
const handlePositionUpdate = () => updateSidebarPanelMenuPosition();
window.addEventListener("resize", handlePositionUpdate);
window.addEventListener("scroll", handlePositionUpdate, true);
return () => {
window.removeEventListener("resize", handlePositionUpdate);
window.removeEventListener("scroll", handlePositionUpdate, true);
};
}, [isWorkspaceMenuOpen, updateSidebarPanelMenuPosition, variant]);
return (
<Menu
as="div"
@ -121,13 +136,15 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
})}
>
{({ open, close }: { open: boolean; close: () => void }) => {
// Update local state directly
if (isWorkspaceMenuOpen !== open) {
setIsWorkspaceMenuOpen(open);
}
return (
<>
<WorkspaceMenuStateSync
open={open}
variant={variant}
sidebarPanelButtonRef={sidebarPanelButtonRef}
onSidebarDropdownToggle={toggleAnySidebarDropdown}
onSidebarPanelPositionChange={setSidebarPanelMenuPosition}
/>
{variant === "sidebar" && (
<Menu.Button
className={cn("flex size-8 w-full items-center justify-center rounded-md", {

View File

@ -253,4 +253,62 @@
outline: none !important;
box-shadow: none !important;
}
.nodedc-modal-field {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
rgba(255, 255, 255, 0.028);
border: 1px solid transparent;
border-radius: 1.25rem;
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
box-shadow: none;
outline: none !important;
transition:
background 160ms ease,
opacity 160ms ease;
}
.nodedc-modal-field:hover,
.nodedc-modal-field:focus-within {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.038) 0%, rgba(255, 255, 255, 0.016) 100%),
rgba(255, 255, 255, 0.04);
}
.nodedc-modal-input {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
rgba(255, 255, 255, 0.028) !important;
border: 1px solid transparent !important;
border-radius: 1.25rem !important;
box-shadow: none !important;
outline: none !important;
}
.nodedc-modal-input:focus,
.nodedc-modal-input:focus-visible {
box-shadow: none !important;
outline: none !important;
}
.nodedc-modal-editor {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
rgba(255, 255, 255, 0.028) !important;
border: 1px solid transparent !important;
border-radius: 1.5rem !important;
overflow: hidden;
box-shadow: none !important;
}
.nodedc-modal-editor .ProseMirror {
min-height: 10.5rem;
padding: 1rem 1.25rem 1.25rem !important;
outline: none !important;
}
.nodedc-modal-editor .ProseMirror p.is-editor-empty:first-child::before {
color: rgba(255, 255, 255, 0.42) !important;
}
}

View File

@ -318,6 +318,7 @@ export default {
target_project: "Target external contour",
selected_target: "Selected contour",
assignee: "Assignee",
priority: "Choose priority",
due_date: "Due date",
},
modal: {

View File

@ -475,6 +475,7 @@ export default {
target_project: "Целевой внешний контур",
selected_target: "Выбранный контур",
assignee: "Исполнитель",
priority: "Выберите приоритет",
due_date: "Срок",
},
modal: {