REALTIME - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: обновление доступов workspace

This commit is contained in:
DCCONSTRUCTIONS 2026-05-12 22:26:48 +03:00
parent 5e7c9e08a0
commit 268ab2c9b9
5 changed files with 119 additions and 3 deletions

View File

@ -30,6 +30,7 @@ from plane.app.permissions import (
WorkSpaceBasePermission, WorkSpaceBasePermission,
WorkspaceEntityPermission, WorkspaceEntityPermission,
) )
from plane.app.realtime.nodedc_events import publish_nodedc_event_to_users_on_commit
# Module imports # Module imports
from plane.app.serializers import WorkSpaceSerializer, WorkspaceThemeSerializer from plane.app.serializers import WorkSpaceSerializer, WorkspaceThemeSerializer
@ -197,6 +198,14 @@ class WorkSpaceViewSet(BaseViewSet):
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
# Get the workspace # Get the workspace
workspace = self.get_object() workspace = self.get_object()
workspace_member_ids = list(
WorkspaceMember.objects.filter(
workspace_id=workspace.id,
is_active=True,
member__is_bot=False,
deleted_at__isnull=True,
).values_list("member_id", flat=True)
)
self.remove_last_workspace_ids_from_user_settings(workspace.id) self.remove_last_workspace_ids_from_user_settings(workspace.id)
track_event.delay( track_event.delay(
user_id=request.user.id, user_id=request.user.id,
@ -211,7 +220,16 @@ class WorkSpaceViewSet(BaseViewSet):
"deleted_at": str(timezone.now().isoformat()), "deleted_at": str(timezone.now().isoformat()),
}, },
) )
return super().destroy(request, *args, **kwargs) response = super().destroy(request, *args, **kwargs)
publish_nodedc_event_to_users_on_commit(
"workspace.deleted",
workspace_member_ids,
{
"workspace_id": str(workspace.id),
"workspace_slug": workspace.slug,
},
)
return response
class UserWorkSpacesEndpoint(BaseAPIView): class UserWorkSpacesEndpoint(BaseAPIView):

View File

@ -13,6 +13,8 @@ from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from plane.app.realtime.issue_events import publish_assignee_cleanup_issue_events_on_commit
from plane.app.realtime.nodedc_events import publish_nodedc_workspace_event_on_commit
from plane.authentication.utils.host import user_ip from plane.authentication.utils.host import user_ip
from plane.db.models import ExternalIdentityLink, IssueAssignee, ProjectMember, Session, User, WorkspaceMember from plane.db.models import ExternalIdentityLink, IssueAssignee, ProjectMember, Session, User, WorkspaceMember
@ -240,10 +242,57 @@ def delete_queryset(queryset):
def revoke_user_tasker_access(user): def revoke_user_tasker_access(user):
workspace_memberships = list(
WorkspaceMember.objects.filter(member=user)
.select_related("workspace")
.only("id", "workspace_id", "workspace__id", "workspace__slug")
)
project_memberships = list(
ProjectMember.objects.filter(member=user)
.select_related("workspace", "project")
.only("id", "workspace_id", "project_id", "workspace__id", "workspace__slug")
)
workspace_by_id = {}
project_ids_by_workspace_id = {}
for membership in workspace_memberships:
workspace_by_id[membership.workspace_id] = membership.workspace
project_ids_by_workspace_id.setdefault(membership.workspace_id, set())
for membership in project_memberships:
workspace_by_id[membership.workspace_id] = membership.workspace
project_ids_by_workspace_id.setdefault(membership.workspace_id, set()).add(membership.project_id)
publish_assignee_cleanup_issue_events_on_commit(project_id=membership.project_id, assignee_id=user.id)
deleted_issue_assignees = delete_queryset(IssueAssignee.objects.filter(assignee=user)) deleted_issue_assignees = delete_queryset(IssueAssignee.objects.filter(assignee=user))
deleted_project_memberships = delete_queryset(ProjectMember.objects.filter(member=user)) deleted_project_memberships = delete_queryset(ProjectMember.objects.filter(member=user))
deleted_workspace_memberships = delete_queryset(WorkspaceMember.objects.filter(member=user)) deleted_workspace_memberships = delete_queryset(WorkspaceMember.objects.filter(member=user))
for workspace_id, workspace in workspace_by_id.items():
project_ids = [str(project_id) for project_id in project_ids_by_workspace_id.get(workspace_id, set())]
publish_nodedc_workspace_event_on_commit(
workspace,
"workspace_member.deleted",
payload={
"member_id": str(user.id),
"project_ids": project_ids,
"source": "launcher",
},
extra_user_ids=[user.id],
)
for membership in project_memberships:
publish_nodedc_workspace_event_on_commit(
membership.workspace,
"project_member.deleted",
payload={
"project_id": str(membership.project_id),
"member_id": str(user.id),
"source": "launcher",
},
extra_user_ids=[user.id],
)
return { return {
"workspaceMemberships": deleted_workspace_memberships, "workspaceMemberships": deleted_workspace_memberships,
"projectMemberships": deleted_project_memberships, "projectMemberships": deleted_project_memberships,

View File

@ -10,8 +10,10 @@ import { useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { X } from "lucide-react"; import { X } from "lucide-react";
// plane imports // plane imports
import { ENotificationLoader, ENotificationQueryParamType } from "@plane/constants";
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks // hooks
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
import { useWorkspace } from "@/hooks/store/use-workspace"; import { useWorkspace } from "@/hooks/store/use-workspace";
// local imports // local imports
import { NotificationsRoot } from "./root"; import { NotificationsRoot } from "./root";
@ -31,6 +33,7 @@ const getInitialOpenState = () => {
export const WorkspaceNotificationsModal = observer(function WorkspaceNotificationsModal() { export const WorkspaceNotificationsModal = observer(function WorkspaceNotificationsModal() {
const [isOpen, setIsOpen] = useState(getInitialOpenState); const [isOpen, setIsOpen] = useState(getInitialOpenState);
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { getNotifications, setCurrentSelectedNotificationId } = useWorkspaceNotifications();
useEffect(() => { useEffect(() => {
const syncFromLocation = () => { const syncFromLocation = () => {
@ -54,6 +57,17 @@ export const WorkspaceNotificationsModal = observer(function WorkspaceNotificati
}; };
}, []); }, []);
useEffect(() => {
if (!isOpen || !currentWorkspace?.slug) return;
setCurrentSelectedNotificationId(undefined);
void getNotifications(
currentWorkspace.slug,
ENotificationLoader.MUTATION_LOADER,
ENotificationQueryParamType.CURRENT
);
}, [currentWorkspace?.slug, getNotifications, isOpen, setCurrentSelectedNotificationId]);
const handleClose = () => closeWorkspaceNotificationsModal(); const handleClose = () => closeWorkspaceNotificationsModal();
return ( return (

View File

@ -44,10 +44,11 @@ type WorkspaceMenuStateSyncProps = {
sidebarPanelButtonRef: RefObject<HTMLButtonElement | null>; sidebarPanelButtonRef: RefObject<HTMLButtonElement | null>;
onSidebarDropdownToggle: (value: boolean) => void; onSidebarDropdownToggle: (value: boolean) => void;
onSidebarPanelPositionChange: (position: { left: number; top: number; width: number } | null) => void; onSidebarPanelPositionChange: (position: { left: number; top: number; width: number } | null) => void;
onOpen?: () => void;
}; };
function WorkspaceMenuStateSync(props: WorkspaceMenuStateSyncProps) { function WorkspaceMenuStateSync(props: WorkspaceMenuStateSyncProps) {
const { open, variant, sidebarPanelButtonRef, onSidebarDropdownToggle, onSidebarPanelPositionChange } = props; const { open, variant, sidebarPanelButtonRef, onOpen, onSidebarDropdownToggle, onSidebarPanelPositionChange } = props;
const updateSidebarPanelMenuPosition = useCallback(() => { const updateSidebarPanelMenuPosition = useCallback(() => {
if ( if (
@ -72,6 +73,10 @@ function WorkspaceMenuStateSync(props: WorkspaceMenuStateSyncProps) {
onSidebarDropdownToggle(open); onSidebarDropdownToggle(open);
}, [onSidebarDropdownToggle, open]); }, [onSidebarDropdownToggle, open]);
useEffect(() => {
if (open) onOpen?.();
}, [onOpen, open]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!open || !["sidebar-panel", "toolbar", "expanded-toolbar"].includes(variant)) { if (!open || !["sidebar-panel", "toolbar", "expanded-toolbar"].includes(variant)) {
onSidebarPanelPositionChange(null); onSidebarPanelPositionChange(null);
@ -102,7 +107,7 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
const { signOut } = useUser(); const { signOut } = useUser();
const { updateUserProfile } = useUserProfile(); const { updateUserProfile } = useUserProfile();
const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace(); const { currentWorkspace: activeWorkspace, workspaces } = useWorkspace();
const { data: nodedcWorkspacePolicy } = useSWR(currentUser ? "NODEDC_WORKSPACE_POLICY" : null, () => const { data: nodedcWorkspacePolicy, mutate: mutateNodeDCWorkspacePolicy } = useSWR(currentUser ? "NODEDC_WORKSPACE_POLICY" : null, () =>
workspaceService.getNodeDCWorkspacePolicy() workspaceService.getNodeDCWorkspacePolicy()
); );
// derived values // derived values
@ -118,6 +123,9 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
} | null>(null); } | null>(null);
const sidebarPanelButtonRef = useRef<HTMLButtonElement>(null); const sidebarPanelButtonRef = useRef<HTMLButtonElement>(null);
const handleWorkspaceMenuOpen = useCallback(() => {
void mutateNodeDCWorkspacePolicy();
}, [mutateNodeDCWorkspacePolicy]);
const handleWorkspaceNavigation = (workspace: IWorkspace) => updateUserProfile({ last_workspace_id: workspace?.id }); const handleWorkspaceNavigation = (workspace: IWorkspace) => updateUserProfile({ last_workspace_id: workspace?.id });
@ -157,6 +165,7 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
variant={variant} variant={variant}
sidebarPanelButtonRef={sidebarPanelButtonRef} sidebarPanelButtonRef={sidebarPanelButtonRef}
onSidebarDropdownToggle={toggleAnySidebarDropdown} onSidebarDropdownToggle={toggleAnySidebarDropdown}
onOpen={handleWorkspaceMenuOpen}
onSidebarPanelPositionChange={setSidebarPanelMenuPosition} onSidebarPanelPositionChange={setSidebarPanelMenuPosition}
/> />
{variant === "sidebar" && ( {variant === "sidebar" && (

View File

@ -99,6 +99,7 @@ export const useNodeDCRealtimeEvents = (enabled: boolean, currentUserId?: string
const refreshWorkspaceScope = async (event: TNodeDCRealtimeEvent) => { const refreshWorkspaceScope = async (event: TNodeDCRealtimeEvent) => {
const workspaceSlug = event.workspace_slug; const workspaceSlug = event.workspace_slug;
if (!workspaceSlug) return; if (!workspaceSlug) return;
const projectIds = [...new Set(event.project_ids?.filter(Boolean) ?? [])];
await Promise.allSettled([ await Promise.allSettled([
workspaceRoot.fetchWorkspaces(), workspaceRoot.fetchWorkspaces(),
@ -106,6 +107,9 @@ export const useNodeDCRealtimeEvents = (enabled: boolean, currentUserId?: string
memberRoot.workspace.fetchWorkspaceMemberInvitations(workspaceSlug), memberRoot.workspace.fetchWorkspaceMemberInvitations(workspaceSlug),
projectStore.fetchProjects(workspaceSlug), projectStore.fetchProjects(workspaceSlug),
userStore.permission.fetchUserProjectPermissions(workspaceSlug), userStore.permission.fetchUserProjectPermissions(workspaceSlug),
mutate("NODEDC_WORKSPACE_POLICY"),
mutate(`NODEDC_WORKSPACE_POLICY_${workspaceSlug}`),
...projectIds.map((projectId) => memberRoot.project.fetchProjectMembers(workspaceSlug, projectId, true)),
]); ]);
if ( if (
@ -127,6 +131,8 @@ export const useNodeDCRealtimeEvents = (enabled: boolean, currentUserId?: string
memberRoot.workspace.fetchWorkspaceMembers(workspaceSlug), memberRoot.workspace.fetchWorkspaceMembers(workspaceSlug),
projectStore.fetchProjects(workspaceSlug), projectStore.fetchProjects(workspaceSlug),
userStore.permission.fetchUserProjectPermissions(workspaceSlug), userStore.permission.fetchUserProjectPermissions(workspaceSlug),
mutate("NODEDC_WORKSPACE_POLICY"),
mutate(`NODEDC_WORKSPACE_POLICY_${workspaceSlug}`),
projectId ? memberRoot.project.fetchProjectMembers(workspaceSlug, projectId, true) : Promise.resolve(), projectId ? memberRoot.project.fetchProjectMembers(workspaceSlug, projectId, true) : Promise.resolve(),
]); ]);
@ -140,6 +146,21 @@ export const useNodeDCRealtimeEvents = (enabled: boolean, currentUserId?: string
} }
}; };
const refreshDeletedWorkspaceScope = async (event: TNodeDCRealtimeEvent) => {
const workspaceSlug = event.workspace_slug;
if (!workspaceSlug) return;
await Promise.allSettled([
workspaceRoot.fetchWorkspaces(),
mutate("NODEDC_WORKSPACE_POLICY"),
mutate(`NODEDC_WORKSPACE_POLICY_${workspaceSlug}`),
]);
if (isWorkspacePath(pathnameRef.current, workspaceSlug)) {
router.replace(workspaceRoot.getWorkspaceRedirectionUrl());
}
};
const refreshInviteScope = async (event: TNodeDCRealtimeEvent) => { const refreshInviteScope = async (event: TNodeDCRealtimeEvent) => {
await Promise.allSettled([ await Promise.allSettled([
mutate("USER_WORKSPACE_INVITATIONS_NOTICE"), mutate("USER_WORKSPACE_INVITATIONS_NOTICE"),
@ -163,6 +184,11 @@ export const useNodeDCRealtimeEvents = (enabled: boolean, currentUserId?: string
const handleEvent = async (event: TNodeDCRealtimeEvent) => { const handleEvent = async (event: TNodeDCRealtimeEvent) => {
if (!rememberEvent(event.event_id)) return; if (!rememberEvent(event.event_id)) return;
if (event.type === "workspace.deleted") {
await refreshDeletedWorkspaceScope(event);
return;
}
if (event.type?.startsWith("workspace_member.")) { if (event.type?.startsWith("workspace_member.")) {
await refreshWorkspaceScope(event); await refreshWorkspaceScope(event);
return; return;