UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: фикс popup рабочего пространства, sidebar-search и полировка модалки внешнего запроса
This commit is contained in:
parent
01eb3d4c8a
commit
21581373cd
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -475,6 +475,7 @@ export default {
|
|||
target_project: "Целевой внешний контур",
|
||||
selected_target: "Выбранный контур",
|
||||
assignee: "Исполнитель",
|
||||
priority: "Выберите приоритет",
|
||||
due_date: "Срок",
|
||||
},
|
||||
modal: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue