REALTIME - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: обновление доступов workspace
This commit is contained in:
parent
5e7c9e08a0
commit
268ab2c9b9
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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" && (
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue