UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: фикс popup рабочего пространства, sidebar-search и полировка модалки внешнего запроса
This commit is contained in:
parent
01eb3d4c8a
commit
21581373cd
|
|
@ -22,7 +22,7 @@ export function ExternalContourCreateModalRoot(props: Props) {
|
||||||
isOpen={modalState}
|
isOpen={modalState}
|
||||||
position={EModalPosition.CENTER}
|
position={EModalPosition.CENTER}
|
||||||
width={EModalWidth.XXXXL}
|
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} />
|
<ExternalContoursCreateRoot workspaceSlug={workspaceSlug} projectId={projectId} handleModalClose={handleModalClose} />
|
||||||
</ModalCore>
|
</ModalCore>
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { CalendarDays, SignalHigh } from "lucide-react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { ISSUE_PRIORITIES } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { Badge } from "@plane/propel/badge";
|
import { LabelPropertyIcon, MembersPropertyIcon, PriorityIcon, ProjectIcon } from "@plane/propel/icons";
|
||||||
import { MembersPropertyIcon } from "@plane/propel/icons";
|
|
||||||
import type { TIssue, TIssuePriorities } from "@plane/types";
|
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 { DateDropdown } from "@/components/dropdowns/date";
|
||||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||||
import { MemberDropdownBase } from "@/components/dropdowns/member/base";
|
import { MemberDropdownBase } from "@/components/dropdowns/member/base";
|
||||||
|
|
@ -31,30 +31,31 @@ export const ExternalContoursCreateProperties = observer(function ExternalContou
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const { targetProjectIds, getTargetOptionsByProjectId, getTargetProjectById } = useProjectExternalContours();
|
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 selectedTargetProject = data.target_project_id ? getTargetProjectById(data.target_project_id) : undefined;
|
||||||
const selectedTargetOptions = getTargetOptionsByProjectId(data.target_project_id);
|
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) =>
|
const getTargetLabelById = (labelId: string) =>
|
||||||
selectedTargetOptions?.labels?.find((label) => label.id === labelId) ?? null;
|
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 (
|
return (
|
||||||
<div className="relative flex flex-wrap items-center gap-2">
|
<div className="relative flex flex-wrap items-center gap-2.5">
|
||||||
<div className="rounded-md border border-subtle bg-surface-2 px-3 py-1.5 text-11 text-secondary">
|
<div className={cn(controlClassName, "min-w-[16rem] justify-start")}>
|
||||||
<span className="mr-2 text-tertiary">{t("external_contours_page.form.source_project")}</span>
|
<span className="text-tertiary">{t("external_contours_page.form.source_project")}</span>
|
||||||
<Badge variant="neutral">{currentProjectName || "NODE.DC"}</Badge>
|
<span className="truncate text-primary">{currentProjectName || "NODE.DC"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-7">
|
<div className="h-10">
|
||||||
<ProjectDropdownBase
|
<ProjectDropdownBase
|
||||||
value={data.target_project_id ?? null}
|
value={data.target_project_id ?? null}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
|
@ -67,41 +68,36 @@ export const ExternalContoursCreateProperties = observer(function ExternalContou
|
||||||
multiple={false}
|
multiple={false}
|
||||||
projectIds={targetProjectIds}
|
projectIds={targetProjectIds}
|
||||||
getProjectById={getTargetProjectById}
|
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")}
|
placeholder={t("external_contours_page.form.target_project")}
|
||||||
disabled={targetProjectIds.length === 0}
|
disabled={targetProjectIds.length === 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedTargetProject && (
|
<div className="h-10">
|
||||||
<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">
|
|
||||||
<MemberDropdownBase
|
<MemberDropdownBase
|
||||||
value={data.assignee_ids || []}
|
value={assigneeIds}
|
||||||
onChange={(assigneeIds) => handleData("assignee_ids", assigneeIds)}
|
onChange={(nextAssigneeIds) => handleData("assignee_ids", nextAssigneeIds)}
|
||||||
getUserDetails={getUserDetails}
|
getUserDetails={getUserDetails}
|
||||||
memberIds={selectedTargetOptions?.member_ids ?? []}
|
memberIds={selectedTargetOptions?.member_ids ?? []}
|
||||||
button={
|
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">
|
<div className={cn(controlClassName, "min-w-[12rem] justify-start")}>
|
||||||
<ButtonAvatars showTooltip={false} userIds={data.assignee_ids || []} icon={MembersPropertyIcon} />
|
<ButtonAvatars showTooltip={false} userIds={assigneeIds} icon={MembersPropertyIcon} />
|
||||||
<span className="flex-grow truncate text-left text-body-xs-medium leading-5">{assigneeLabel}</span>
|
<span className={cn("truncate", assigneeIds.length > 0 ? "text-primary" : "text-tertiary")}>
|
||||||
|
{assigneeLabel}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonVariant={(data.assignee_ids || []).length > 0 ? "transparent-without-text" : "border-with-text"}
|
buttonVariant="transparent-without-text"
|
||||||
buttonClassName={(data.assignee_ids || []).length > 0 ? "hover:bg-transparent" : ""}
|
|
||||||
optionsClassName="z-[60]"
|
optionsClassName="z-[60]"
|
||||||
placeholder={t("external_contours_page.form.assignee")}
|
placeholder={t("external_contours_page.form.assignee")}
|
||||||
disabled={!data.target_project_id || !selectedTargetOptions}
|
disabled={!data.target_project_id || !selectedTargetOptions}
|
||||||
|
|
@ -109,21 +105,67 @@ export const ExternalContoursCreateProperties = observer(function ExternalContou
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<WorkItemLabelSelectBase
|
||||||
value={data.label_ids || []}
|
value={data.label_ids || []}
|
||||||
onChange={(labelIds) => handleData("label_ids", labelIds)}
|
onChange={(labelIds) => handleData("label_ids", labelIds)}
|
||||||
getLabelById={getTargetLabelById}
|
getLabelById={getTargetLabelById}
|
||||||
labelIds={targetLabelIds}
|
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}
|
disabled={!data.target_project_id || !selectedTargetOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-7">
|
<div className="h-10">
|
||||||
<DateDropdown
|
<DateDropdown
|
||||||
value={data.target_date || null}
|
value={data.target_date || null}
|
||||||
onChange={(date) => handleData("target_date", date ? renderFormattedPayloadDate(date) : "")}
|
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")}
|
placeholder={t("external_contours_page.form.due_date")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -126,57 +126,67 @@ export const ExternalContoursCreateRoot = observer(function ExternalContoursCrea
|
||||||
if (!workspaceSlug || !projectId || !workspaceId) return <></>;
|
if (!workspaceSlug || !projectId || !workspaceId) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full gap-2 bg-transparent">
|
<form onSubmit={handleFormSubmit} className="flex w-full flex-col gap-6 px-6 py-6">
|
||||||
<div className="w-full rounded-lg">
|
<div className="space-y-5">
|
||||||
<form onSubmit={handleFormSubmit} className="flex w-full flex-col">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="space-y-5 rounded-t-lg bg-surface-1 p-5">
|
<h3 className="text-18 font-medium text-secondary">{t("external_contours_page.modal.title")}</h3>
|
||||||
<div className="flex items-center justify-between gap-2">
|
</div>
|
||||||
<h3 className="text-18 font-medium text-secondary">{t("external_contours_page.modal.title")}</h3>
|
<div className="space-y-4">
|
||||||
</div>
|
<InboxIssueTitle
|
||||||
<div className="space-y-3">
|
data={formData}
|
||||||
<InboxIssueTitle
|
handleData={handleFormData as any}
|
||||||
data={formData}
|
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
|
||||||
handleData={handleFormData as any}
|
inputClassName="nodedc-modal-input !px-4 !py-3 !text-[15px]"
|
||||||
isTitleLengthMoreThan255Character={isTitleLengthMoreThan255Character}
|
/>
|
||||||
/>
|
<InboxIssueDescription
|
||||||
<InboxIssueDescription
|
workspaceSlug={workspaceSlug}
|
||||||
workspaceSlug={workspaceSlug}
|
projectId={projectId}
|
||||||
projectId={projectId}
|
workspaceId={workspaceId}
|
||||||
workspaceId={workspaceId}
|
data={formData}
|
||||||
data={formData}
|
handleData={handleFormData as any}
|
||||||
handleData={handleFormData as any}
|
editorRef={descriptionEditorRef}
|
||||||
editorRef={descriptionEditorRef}
|
containerClassName="nodedc-modal-editor min-h-[180px] !border-none !bg-transparent !p-0"
|
||||||
containerClassName="min-h-[150px] border-[0.5px] border-subtle-1 bg-layer-2 py-3"
|
onAssetUpload={() => {}}
|
||||||
onAssetUpload={() => {}}
|
/>
|
||||||
/>
|
<ExternalContoursCreateProperties
|
||||||
<ExternalContoursCreateProperties
|
currentProjectName={currentProjectDetails?.name}
|
||||||
currentProjectName={currentProjectDetails?.name}
|
data={formData}
|
||||||
data={formData}
|
handleData={handleFormData as any}
|
||||||
handleData={handleFormData as any}
|
/>
|
||||||
/>
|
</div>
|
||||||
</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>
|
|
||||||
</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 type { TIssue } from "@plane/types";
|
||||||
import { Input } from "@plane/ui";
|
import { Input } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { getTabIndex } from "@plane/utils";
|
import { cn, getTabIndex } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
|
||||||
|
|
@ -19,10 +19,11 @@ type TInboxIssueTitle = {
|
||||||
data: Partial<TIssue>;
|
data: Partial<TIssue>;
|
||||||
handleData: (issueKey: keyof Partial<TIssue>, issueValue: Partial<TIssue>[keyof Partial<TIssue>]) => void;
|
handleData: (issueKey: keyof Partial<TIssue>, issueValue: Partial<TIssue>[keyof Partial<TIssue>]) => void;
|
||||||
isTitleLengthMoreThan255Character?: boolean;
|
isTitleLengthMoreThan255Character?: boolean;
|
||||||
|
inputClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InboxIssueTitle = observer(function InboxIssueTitle(props: TInboxIssueTitle) {
|
export const InboxIssueTitle = observer(function InboxIssueTitle(props: TInboxIssueTitle) {
|
||||||
const { data, handleData, isTitleLengthMoreThan255Character } = props;
|
const { data, handleData, isTitleLengthMoreThan255Character, inputClassName } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { isMobile } = usePlatformOS();
|
const { isMobile } = usePlatformOS();
|
||||||
|
|
||||||
|
|
@ -37,7 +38,7 @@ export const InboxIssueTitle = observer(function InboxIssueTitle(props: TInboxIs
|
||||||
value={data?.name}
|
value={data?.name}
|
||||||
onChange={(e) => handleData("name", e.target.value)}
|
onChange={(e) => handleData("name", e.target.value)}
|
||||||
placeholder={t("title")}
|
placeholder={t("title")}
|
||||||
className="w-full text-14"
|
className={cn("w-full text-14", inputClassName)}
|
||||||
tabIndex={getIndex("name")}
|
tabIndex={getIndex("name")}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||||
const width = 320;
|
const width = 320;
|
||||||
const viewportPadding = 16;
|
const viewportPadding = 16;
|
||||||
const left = Math.min(rect.left, window.innerWidth - width - viewportPadding);
|
const left = Math.min(rect.left, window.innerWidth - width - viewportPadding);
|
||||||
const top = rect.top;
|
const top = rect.top + rect.height / 2;
|
||||||
|
|
||||||
setSidebarSearchPosition({
|
setSidebarSearchPosition({
|
||||||
left,
|
left,
|
||||||
|
|
@ -372,6 +372,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
|
||||||
left: `${sidebarSearchPosition.left}px`,
|
left: `${sidebarSearchPosition.left}px`,
|
||||||
top: `${sidebarSearchPosition.top}px`,
|
top: `${sidebarSearchPosition.top}px`,
|
||||||
width: `${sidebarSearchPosition.width}px`,
|
width: `${sidebarSearchPosition.width}px`,
|
||||||
|
transform: "translateY(-50%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export function ShortcutsModal(props: Props) {
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
<Dialog as="div" className="relative z-30" onClose={handleClose}>
|
<Dialog as="div" className="relative z-30" onClose={handleClose}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as="div"
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0"
|
||||||
enterTo="opacity-100"
|
enterTo="opacity-100"
|
||||||
|
|
@ -52,7 +52,7 @@ export function ShortcutsModal(props: Props) {
|
||||||
<div className="fixed inset-0 z-30 overflow-y-auto">
|
<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">
|
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as="div"
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
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}>
|
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={React.Fragment}
|
as="div"
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0"
|
||||||
enterTo="opacity-100"
|
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="fixed inset-0 z-30 overflow-y-auto">
|
||||||
<div className="flex items-center justify-center p-4 sm:p-6 md:p-20">
|
<div className="flex items-center justify-center p-4 sm:p-6 md:p-20">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={React.Fragment}
|
as="div"
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* See the LICENSE file for details.
|
* 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 { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
|
@ -34,6 +34,56 @@ type WorkspaceMenuRootProps = {
|
||||||
variant: "sidebar" | "top-navigation" | "sidebar-panel";
|
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) {
|
export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: WorkspaceMenuRootProps) {
|
||||||
const { variant } = props;
|
const { variant } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
|
|
@ -48,7 +98,6 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
||||||
// translation
|
// translation
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// local state
|
// local state
|
||||||
const [isWorkspaceMenuOpen, setIsWorkspaceMenuOpen] = useState(false);
|
|
||||||
const [sidebarPanelMenuPosition, setSidebarPanelMenuPosition] = useState<{
|
const [sidebarPanelMenuPosition, setSidebarPanelMenuPosition] = useState<{
|
||||||
left: number;
|
left: number;
|
||||||
top: number;
|
top: number;
|
||||||
|
|
@ -77,40 +126,6 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
||||||
const workspacesList = orderWorkspacesList(Object.values(workspaces ?? {}));
|
const workspacesList = orderWorkspacesList(Object.values(workspaces ?? {}));
|
||||||
// TODO: fix workspaces list scroll
|
// 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 (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
as="div"
|
as="div"
|
||||||
|
|
@ -121,13 +136,15 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{({ open, close }: { open: boolean; close: () => void }) => {
|
{({ open, close }: { open: boolean; close: () => void }) => {
|
||||||
// Update local state directly
|
|
||||||
if (isWorkspaceMenuOpen !== open) {
|
|
||||||
setIsWorkspaceMenuOpen(open);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<WorkspaceMenuStateSync
|
||||||
|
open={open}
|
||||||
|
variant={variant}
|
||||||
|
sidebarPanelButtonRef={sidebarPanelButtonRef}
|
||||||
|
onSidebarDropdownToggle={toggleAnySidebarDropdown}
|
||||||
|
onSidebarPanelPositionChange={setSidebarPanelMenuPosition}
|
||||||
|
/>
|
||||||
{variant === "sidebar" && (
|
{variant === "sidebar" && (
|
||||||
<Menu.Button
|
<Menu.Button
|
||||||
className={cn("flex size-8 w-full items-center justify-center rounded-md", {
|
className={cn("flex size-8 w-full items-center justify-center rounded-md", {
|
||||||
|
|
|
||||||
|
|
@ -253,4 +253,62 @@
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
box-shadow: 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",
|
target_project: "Target external contour",
|
||||||
selected_target: "Selected contour",
|
selected_target: "Selected contour",
|
||||||
assignee: "Assignee",
|
assignee: "Assignee",
|
||||||
|
priority: "Choose priority",
|
||||||
due_date: "Due date",
|
due_date: "Due date",
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
|
|
|
||||||
|
|
@ -475,6 +475,7 @@ export default {
|
||||||
target_project: "Целевой внешний контур",
|
target_project: "Целевой внешний контур",
|
||||||
selected_target: "Выбранный контур",
|
selected_target: "Выбранный контур",
|
||||||
assignee: "Исполнитель",
|
assignee: "Исполнитель",
|
||||||
|
priority: "Выберите приоритет",
|
||||||
due_date: "Срок",
|
due_date: "Срок",
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue