ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: единый realtime слой Tasker
Добавлен NODE.DC realtime event stream для workspace/project/member/invite/profile событий. Обновлены frontend stores и live controller. Доработаны settings modal, member dropdown и confirm remove под NODE.DC UX.
This commit is contained in:
@@ -6,7 +6,6 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// types
|
||||
import { Button } from "@plane/propel/button";
|
||||
@@ -22,12 +21,11 @@ type Props = {
|
||||
onSubmit: () => Promise<void>;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const ConfirmProjectMemberRemove = observer(function ConfirmProjectMemberRemove(props: Props) {
|
||||
const { data, onSubmit, isOpen, onClose } = props;
|
||||
// router
|
||||
const { projectId } = useParams();
|
||||
const { data, onSubmit, isOpen, onClose, projectId } = props;
|
||||
// states
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
// store hooks
|
||||
@@ -50,44 +48,62 @@ export const ConfirmProjectMemberRemove = observer(function ConfirmProjectMember
|
||||
if (!projectId) return <></>;
|
||||
|
||||
const isCurrentUser = currentUser?.id === data?.id;
|
||||
const currentProjectDetails = getProjectById(projectId.toString());
|
||||
const currentProjectDetails = getProjectById(projectId);
|
||||
const memberName = data?.display_name || "участника";
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
|
||||
<div className="bg-surface-1 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mx-auto flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-danger-subtle sm:mx-0 sm:h-10 sm:w-10">
|
||||
<AlertTriangle className="h-6 w-6 text-danger-primary" aria-hidden="true" />
|
||||
<ModalCore
|
||||
isOpen={isOpen}
|
||||
handleClose={handleClose}
|
||||
position={EModalPosition.CENTER}
|
||||
width={EModalWidth.XXL}
|
||||
className="overflow-hidden border-0 bg-[rgba(10,10,14,0.96)] p-0 shadow-[0_28px_80px_rgba(0,0,0,0.42)]"
|
||||
>
|
||||
<div className="p-6 sm:p-7">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="grid size-12 shrink-0 place-items-center rounded-[1.2rem] bg-[rgb(var(--nodedc-accent-rgb))]/18 text-[rgb(var(--nodedc-accent-rgb))]">
|
||||
<AlertTriangle className="h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-16 leading-6 font-medium text-primary">
|
||||
{isCurrentUser ? "Leave project?" : `Remove ${data?.display_name}?`}
|
||||
<div className="min-w-0">
|
||||
<p className="text-12 font-semibold tracking-[0.24em] text-tertiary uppercase">NODE.DC Tasker</p>
|
||||
<h3 className="mt-2 text-18 leading-6 font-semibold text-primary">
|
||||
{isCurrentUser ? "Покинуть проект?" : `Удалить ${memberName} из проекта?`}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-13 text-secondary">
|
||||
{isCurrentUser ? (
|
||||
<>
|
||||
Are you sure you want to leave the <span className="font-bold">{currentProjectDetails?.name}</span>{" "}
|
||||
project? You will be able to join the project if invited again or if it{"'"}s public.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Are you sure you want to remove member- <span className="font-bold">{data?.display_name}</span>?
|
||||
They will no longer have access to this project. This action cannot be undone.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-3 text-13 leading-6 text-secondary">
|
||||
{isCurrentUser ? (
|
||||
<>
|
||||
Вы потеряете доступ к проекту{" "}
|
||||
<span className="font-semibold text-primary">{currentProjectDetails?.name}</span>. Вернуться можно
|
||||
будет только после нового приглашения или если проект публичный.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Пользователь <span className="font-semibold text-primary">{memberName}</span> потеряет доступ к
|
||||
проекту. Действие нельзя отменить автоматически.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||
<Button variant="secondary" size="lg" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="error-fill" size="lg" tabIndex={1} onClick={handleDeletion} loading={isDeleteLoading}>
|
||||
{isCurrentUser ? (isDeleteLoading ? "Leaving..." : "Leave") : isDeleteLoading ? "Removing..." : "Remove"}
|
||||
</Button>
|
||||
<div className="mt-7 flex justify-end gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
onClick={handleClose}
|
||||
className="nodedc-modal-secondary-button min-w-[11.5rem] px-8"
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={handleDeletion}
|
||||
loading={isDeleteLoading}
|
||||
className="nodedc-modal-primary-button min-w-[12.5rem] px-8"
|
||||
>
|
||||
{isCurrentUser ? (isDeleteLoading ? "Выходим..." : "Покинуть") : isDeleteLoading ? "Удаляем..." : "Удалить"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalCore>
|
||||
);
|
||||
|
||||
@@ -48,6 +48,7 @@ export const ProjectMemberListItem = observer(function ProjectMemberListItem(pro
|
||||
await leaveProject(workspaceSlug.toString(), projectId.toString())
|
||||
.then(async () => {
|
||||
router.push(`/${workspaceSlug}/projects`);
|
||||
return undefined;
|
||||
})
|
||||
.catch((err) => {
|
||||
setToast({
|
||||
@@ -55,6 +56,7 @@ export const ProjectMemberListItem = observer(function ProjectMemberListItem(pro
|
||||
title: "You can’t leave this project yet.",
|
||||
message: err?.error || "Something went wrong. Please try again.",
|
||||
});
|
||||
return undefined;
|
||||
});
|
||||
} else
|
||||
await removeMemberFromProject(workspaceSlug.toString(), projectId.toString(), memberId).catch((err) =>
|
||||
@@ -73,6 +75,7 @@ export const ProjectMemberListItem = observer(function ProjectMemberListItem(pro
|
||||
<ConfirmProjectMemberRemove
|
||||
isOpen={removeMemberModal !== null}
|
||||
onClose={() => setRemoveMemberModal(null)}
|
||||
projectId={projectId}
|
||||
data={{ id: removeMemberModal.member.id, display_name: removeMemberModal.member.display_name || "" }}
|
||||
onSubmit={() => handleRemove(removeMemberModal.member.id)}
|
||||
/>
|
||||
@@ -82,7 +85,7 @@ export const ProjectMemberListItem = observer(function ProjectMemberListItem(pro
|
||||
columns={columns}
|
||||
data={(memberDetails?.filter((member): member is IProjectMemberDetails => member !== null) ?? []) as any}
|
||||
keyExtractor={(rowData) => rowData?.member.id ?? ""}
|
||||
tableClassName="nodedc-settings-table-surface w-max table-auto border-separate border-spacing-0 overflow-visible"
|
||||
tableClassName="nodedc-settings-table-surface min-w-full table-auto border-separate border-spacing-0 overflow-visible"
|
||||
tHeadClassName="border-b border-white/6"
|
||||
thClassName="text-left font-medium divide-x-0 text-placeholder"
|
||||
tBodyClassName="divide-y-0"
|
||||
|
||||
@@ -69,12 +69,12 @@ export const MemberSelect = observer(function MemberSelect(props: Props) {
|
||||
<SearchSelectionDropdown
|
||||
value={value}
|
||||
label={
|
||||
<div className="flex h-3.5 items-center gap-2">
|
||||
<div className="flex min-h-5 min-w-0 items-center gap-2">
|
||||
{selectedOption && (
|
||||
<Avatar name={selectedOption.member?.display_name} src={getFileURL(selectedOption.member?.avatar_url)} />
|
||||
)}
|
||||
{selectedOption ? (
|
||||
selectedOption.member?.display_name
|
||||
<span className="truncate">{selectedOption.member?.display_name}</span>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Ban className="h-3.5 w-3.5 rotate-90 text-placeholder" />
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
import { PROJECT_DETAILS } from "@/constants/fetch-keys";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
// local imports
|
||||
import { MemberSelect } from "./member-select";
|
||||
@@ -60,6 +61,9 @@ export const ProjectSettingsMemberDefaults = observer(function ProjectSettingsMe
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
const {
|
||||
project: { fetchProjectMembers, getProjectMemberFetchStatus },
|
||||
} = useMember();
|
||||
const { currentProjectDetails, fetchProjectDetails, updateProject } = useProject();
|
||||
// derived values
|
||||
const isAdmin = allowPermissions(
|
||||
@@ -76,6 +80,14 @@ export const ProjectSettingsMemberDefaults = observer(function ProjectSettingsMe
|
||||
workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug, projectId) : null
|
||||
);
|
||||
|
||||
const hasFetchedProjectMembers = getProjectMemberFetchStatus(projectId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceSlug || !projectId || hasFetchedProjectMembers) return;
|
||||
|
||||
void fetchProjectMembers(workspaceSlug, projectId, true).catch(console.error);
|
||||
}, [fetchProjectMembers, hasFetchedProjectMembers, projectId, workspaceSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentProjectDetails) return;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||
import { observer } from "mobx-react";
|
||||
@@ -194,7 +193,7 @@ export const ProjectSettingsModal = observer(function ProjectSettingsModal() {
|
||||
handleClose={handleClose}
|
||||
position={EModalPosition.CENTER}
|
||||
width={EModalWidth.VIIXL}
|
||||
className="h-[88vh] max-h-[920px] overflow-hidden border-0 bg-[rgba(10,10,14,0.96)] shadow-[0_28px_80px_rgba(0,0,0,0.42)]"
|
||||
className="h-[88vh] max-h-[920px] !max-w-[calc(100vw-1.5rem)] overflow-hidden border-0 bg-[rgba(10,10,14,0.96)] shadow-[0_28px_80px_rgba(0,0,0,0.42)] sm:!max-w-[calc(100vw-2rem)] xl:!max-w-[88rem]"
|
||||
>
|
||||
{workspaceSlug && activeProjectId ? (
|
||||
<ProjectAuthWrapper workspaceSlug={workspaceSlug} projectId={activeProjectId}>
|
||||
@@ -233,7 +232,7 @@ export const ProjectSettingsModal = observer(function ProjectSettingsModal() {
|
||||
size="sm"
|
||||
className="min-h-0 flex-1 overflow-y-auto"
|
||||
>
|
||||
<div className="mx-auto w-full max-w-[74rem] px-5 pb-7 lg:px-8">
|
||||
<div className="mx-auto w-full max-w-[82rem] px-5 pb-7 lg:px-6">
|
||||
<ProjectSettingsModalContent
|
||||
activeTab={activeTab}
|
||||
projectId={activeProjectId}
|
||||
|
||||
@@ -71,11 +71,22 @@ export const ConfirmWorkspaceMemberRemove = observer(function ConfirmWorkspaceMe
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||
<Button variant="secondary" size="lg" onClick={handleClose}>
|
||||
<div className="flex justify-end gap-3 p-4 sm:px-6">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
onClick={handleClose}
|
||||
className="nodedc-modal-secondary-button min-w-[11.5rem] px-8"
|
||||
>
|
||||
Отменить
|
||||
</Button>
|
||||
<Button variant="primary" size="lg" tabIndex={1} onClick={handleDeletion} loading={isRemoving}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={handleDeletion}
|
||||
loading={isRemoving}
|
||||
className="nodedc-modal-primary-button min-w-[12.5rem] px-8"
|
||||
>
|
||||
{currentUser?.id === userDetails.id
|
||||
? isRemoving
|
||||
? "Выход..."
|
||||
|
||||
@@ -91,7 +91,7 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
|
||||
if (isEmpty(columns)) return <MembersLayoutLoader />;
|
||||
|
||||
return (
|
||||
<div className="horizontal-scrollbar scrollbar-sm w-full overflow-x-auto overflow-y-hidden rounded-[1.2rem]">
|
||||
<div className="w-full overflow-visible rounded-[1.2rem]">
|
||||
{removeMemberModal && (
|
||||
<ConfirmWorkspaceMemberRemove
|
||||
isOpen={removeMemberModal.member.id.length > 0}
|
||||
@@ -109,7 +109,7 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
|
||||
(memberDetails?.filter((member): member is IWorkspaceMember => member !== null) ?? []) as unknown as RowData[]
|
||||
}
|
||||
keyExtractor={(rowData) => rowData?.member.id ?? ""}
|
||||
tableClassName="nodedc-settings-table-surface w-max table-auto border-separate border-spacing-0 overflow-visible"
|
||||
tableClassName="nodedc-settings-table-surface min-w-full table-auto border-separate border-spacing-0 overflow-visible"
|
||||
tHeadClassName="border-b border-white/6"
|
||||
thClassName="text-left font-medium divide-x-0 text-placeholder"
|
||||
tBodyClassName="divide-y-0"
|
||||
|
||||
@@ -49,7 +49,14 @@ import {
|
||||
const HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["billing-and-plans"]);
|
||||
const LAUNCHER_MANAGED_HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["members"]);
|
||||
const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["ai-voice-tasker"]);
|
||||
const MODAL_TABS = new Set<TWorkspaceSettingsTabs>(["general", "members", "export", "storage", "webhooks", "ai-voice-tasker"]);
|
||||
const MODAL_TABS = new Set<TWorkspaceSettingsTabs>([
|
||||
"general",
|
||||
"members",
|
||||
"export",
|
||||
"storage",
|
||||
"webhooks",
|
||||
"ai-voice-tasker",
|
||||
]);
|
||||
const workspaceAIService = new WorkspaceAIService();
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
@@ -77,7 +84,11 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
|
||||
const { t } = useTranslation();
|
||||
const canLoadVoiceTaskerEntitlement =
|
||||
!!currentWorkspace?.slug &&
|
||||
allowPermissions(WORKSPACE_SETTINGS["ai-voice-tasker"].access, EUserPermissionsLevel.WORKSPACE, currentWorkspace.slug);
|
||||
allowPermissions(
|
||||
WORKSPACE_SETTINGS["ai-voice-tasker"].access,
|
||||
EUserPermissionsLevel.WORKSPACE,
|
||||
currentWorkspace.slug
|
||||
);
|
||||
const { data: aiSettings, isLoading: isVoiceTaskerEntitlementLoading } = useSWR(
|
||||
canLoadVoiceTaskerEntitlement ? `WORKSPACE_AI_SETTINGS_${currentWorkspace.slug}` : null,
|
||||
() => workspaceAIService.retrieveSettings(currentWorkspace?.slug as string)
|
||||
@@ -94,7 +105,9 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
|
||||
const tab = getWorkspaceSettingsModalTabFromSearch(window.location.search);
|
||||
setIsOpen(Boolean(tab));
|
||||
if (tab) setActiveTab(tab);
|
||||
setActiveWebhookId(tab === "webhooks" ? getWorkspaceSettingsWebhookIdFromSearch(window.location.search) : undefined);
|
||||
setActiveWebhookId(
|
||||
tab === "webhooks" ? getWorkspaceSettingsWebhookIdFromSearch(window.location.search) : undefined
|
||||
);
|
||||
};
|
||||
|
||||
const handleModalEvent = (event: Event) => {
|
||||
@@ -162,7 +175,9 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
|
||||
}
|
||||
|
||||
if (activeTab === "webhooks" && currentWorkspace?.slug) {
|
||||
return <WorkspaceWebhooksSettingsContent selectedWebhookId={activeWebhookId} workspaceSlug={currentWorkspace.slug} />;
|
||||
return (
|
||||
<WorkspaceWebhooksSettingsContent selectedWebhookId={activeWebhookId} workspaceSlug={currentWorkspace.slug} />
|
||||
);
|
||||
}
|
||||
|
||||
return <WorkspaceDetails />;
|
||||
@@ -178,7 +193,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
|
||||
handleClose={handleClose}
|
||||
position={EModalPosition.CENTER}
|
||||
width={EModalWidth.VIIXL}
|
||||
className="h-[88vh] max-h-[920px] overflow-hidden border-0 bg-[rgba(10,10,14,0.96)] shadow-[0_28px_80px_rgba(0,0,0,0.42)]"
|
||||
className="h-[88vh] max-h-[920px] !max-w-[calc(100vw-1.5rem)] overflow-hidden border-0 bg-[rgba(10,10,14,0.96)] shadow-[0_28px_80px_rgba(0,0,0,0.42)] sm:!max-w-[calc(100vw-2rem)] xl:!max-w-[88rem]"
|
||||
>
|
||||
<div className="flex h-full min-h-0">
|
||||
<div className="hidden h-full w-[296px] shrink-0 md:block">
|
||||
@@ -212,7 +227,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
|
||||
</div>
|
||||
|
||||
<ScrollArea scrollType="hover" orientation="vertical" size="sm" className="min-h-0 flex-1 overflow-y-auto">
|
||||
<div className="mx-auto w-full max-w-[74rem] px-5 pb-7 lg:px-8">{renderContent()}</div>
|
||||
<div className="mx-auto w-full max-w-[78rem] px-5 pb-7 lg:px-8">{renderContent()}</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -260,7 +275,7 @@ function WorkspaceModalSidebar({
|
||||
|
||||
return (
|
||||
<div key={category} className="shrink-0 py-3.5 first:pt-0 last:pb-0">
|
||||
<div className="px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-tertiary">
|
||||
<div className="px-3 py-1.5 text-[11px] font-semibold tracking-[0.18em] text-tertiary uppercase">
|
||||
{t(category)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
|
||||
Reference in New Issue
Block a user