mirror of
https://github.com/makeplane/plane.git
synced 2025-12-16 11:57:56 +01:00
[WEB-3964] refactor: permission layer (#7094)
* refactor: permission layer * refactor: add original_role to project member serializer * chore: minor fixes related to permission layer * fix: strict type checking while checking user permissions
This commit is contained in:
@@ -148,11 +148,13 @@ class ProjectMemberAdminSerializer(BaseSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
|
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
|
||||||
|
original_role = serializers.IntegerField(source='role', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProjectMember
|
model = ProjectMember
|
||||||
fields = ("id", "role", "member", "project", "created_at")
|
fields = ("id", "role", "member", "project", "original_role", "created_at")
|
||||||
read_only_fields = ["created_at"]
|
read_only_fields = ["original_role", "created_at"]
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberInviteSerializer(BaseSerializer):
|
class ProjectMemberInviteSerializer(BaseSerializer):
|
||||||
|
|||||||
@@ -848,6 +848,7 @@
|
|||||||
"live": "Živě",
|
"live": "Živě",
|
||||||
"change_history": "Historie změn",
|
"change_history": "Historie změn",
|
||||||
"coming_soon": "Již brzy",
|
"coming_soon": "Již brzy",
|
||||||
|
"member": "Člen",
|
||||||
"members": "Členové",
|
"members": "Členové",
|
||||||
"you": "Vy",
|
"you": "Vy",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -848,6 +848,7 @@
|
|||||||
"live": "Live",
|
"live": "Live",
|
||||||
"change_history": "Änderungsverlauf",
|
"change_history": "Änderungsverlauf",
|
||||||
"coming_soon": "Demnächst verfügbar",
|
"coming_soon": "Demnächst verfügbar",
|
||||||
|
"member": "Mitglied",
|
||||||
"members": "Mitglieder",
|
"members": "Mitglieder",
|
||||||
"you": "Sie",
|
"you": "Sie",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
@@ -2459,4 +2460,4 @@
|
|||||||
"previously_edited_by": "Zuvor bearbeitet von",
|
"previously_edited_by": "Zuvor bearbeitet von",
|
||||||
"edited_by": "Bearbeitet von"
|
"edited_by": "Bearbeitet von"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -690,6 +690,7 @@
|
|||||||
"live": "Live",
|
"live": "Live",
|
||||||
"change_history": "Change History",
|
"change_history": "Change History",
|
||||||
"coming_soon": "Coming soon",
|
"coming_soon": "Coming soon",
|
||||||
|
"member": "Member",
|
||||||
"members": "Members",
|
"members": "Members",
|
||||||
"you": "You",
|
"you": "You",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -851,6 +851,7 @@
|
|||||||
"live": "En vivo",
|
"live": "En vivo",
|
||||||
"change_history": "Historial de cambios",
|
"change_history": "Historial de cambios",
|
||||||
"coming_soon": "Próximamente",
|
"coming_soon": "Próximamente",
|
||||||
|
"member": "Miembro",
|
||||||
"members": "Miembros",
|
"members": "Miembros",
|
||||||
"you": "Tú",
|
"you": "Tú",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -849,6 +849,7 @@
|
|||||||
"live": "En direct",
|
"live": "En direct",
|
||||||
"change_history": "Historique des modifications",
|
"change_history": "Historique des modifications",
|
||||||
"coming_soon": "À venir",
|
"coming_soon": "À venir",
|
||||||
|
"member": "Membre",
|
||||||
"members": "Membres",
|
"members": "Membres",
|
||||||
"you": "Vous",
|
"you": "Vous",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -848,6 +848,7 @@
|
|||||||
"live": "Langsung",
|
"live": "Langsung",
|
||||||
"change_history": "Riwayat Perubahan",
|
"change_history": "Riwayat Perubahan",
|
||||||
"coming_soon": "Segera hadir",
|
"coming_soon": "Segera hadir",
|
||||||
|
"member": "Anggota",
|
||||||
"members": "Anggota",
|
"members": "Anggota",
|
||||||
"you": "Anda",
|
"you": "Anda",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -847,6 +847,7 @@
|
|||||||
"live": "Live",
|
"live": "Live",
|
||||||
"change_history": "Cronologia modifiche",
|
"change_history": "Cronologia modifiche",
|
||||||
"coming_soon": "Prossimamente",
|
"coming_soon": "Prossimamente",
|
||||||
|
"member": "Membro",
|
||||||
"members": "Membri",
|
"members": "Membri",
|
||||||
"you": "Tu",
|
"you": "Tu",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -849,6 +849,7 @@
|
|||||||
"live": "ライブ",
|
"live": "ライブ",
|
||||||
"change_history": "変更履歴",
|
"change_history": "変更履歴",
|
||||||
"coming_soon": "近日公開",
|
"coming_soon": "近日公開",
|
||||||
|
"member": "メンバー",
|
||||||
"members": "メンバー",
|
"members": "メンバー",
|
||||||
"you": "あなた",
|
"you": "あなた",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"live": "라이브",
|
"live": "라이브",
|
||||||
"change_history": "변경 기록",
|
"change_history": "변경 기록",
|
||||||
"coming_soon": "곧 출시",
|
"coming_soon": "곧 출시",
|
||||||
|
"member": "멤버",
|
||||||
"members": "멤버",
|
"members": "멤버",
|
||||||
"you": "나",
|
"you": "나",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"live": "Na żywo",
|
"live": "Na żywo",
|
||||||
"change_history": "Historia zmian",
|
"change_history": "Historia zmian",
|
||||||
"coming_soon": "Wkrótce",
|
"coming_soon": "Wkrótce",
|
||||||
|
"member": "Członek",
|
||||||
"members": "Członkowie",
|
"members": "Członkowie",
|
||||||
"you": "Ty",
|
"you": "Ty",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"live": "Ao vivo",
|
"live": "Ao vivo",
|
||||||
"change_history": "Histórico de alterações",
|
"change_history": "Histórico de alterações",
|
||||||
"coming_soon": "Em breve",
|
"coming_soon": "Em breve",
|
||||||
|
"member": "Membro",
|
||||||
"members": "Membros",
|
"members": "Membros",
|
||||||
"you": "Você",
|
"you": "Você",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -848,6 +848,7 @@
|
|||||||
"live": "În direct",
|
"live": "În direct",
|
||||||
"change_history": "Istoric modificări",
|
"change_history": "Istoric modificări",
|
||||||
"coming_soon": "În curând",
|
"coming_soon": "În curând",
|
||||||
|
"member": "Membru",
|
||||||
"members": "Membri",
|
"members": "Membri",
|
||||||
"you": "Tu",
|
"you": "Tu",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"live": "В прямом эфире",
|
"live": "В прямом эфире",
|
||||||
"change_history": "История изменений",
|
"change_history": "История изменений",
|
||||||
"coming_soon": "Скоро",
|
"coming_soon": "Скоро",
|
||||||
|
"member": "Участник",
|
||||||
"members": "Участники",
|
"members": "Участники",
|
||||||
"you": "Вы",
|
"you": "Вы",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"live": "Živé",
|
"live": "Živé",
|
||||||
"change_history": "História zmien",
|
"change_history": "História zmien",
|
||||||
"coming_soon": "Už čoskoro",
|
"coming_soon": "Už čoskoro",
|
||||||
|
"member": "Člen",
|
||||||
"members": "Členovia",
|
"members": "Členovia",
|
||||||
"you": "Vy",
|
"you": "Vy",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -851,6 +851,7 @@
|
|||||||
"live": "Canlı",
|
"live": "Canlı",
|
||||||
"change_history": "Değişiklik Geçmişi",
|
"change_history": "Değişiklik Geçmişi",
|
||||||
"coming_soon": "Çok Yakında",
|
"coming_soon": "Çok Yakında",
|
||||||
|
"member": "Üye",
|
||||||
"members": "Üyeler",
|
"members": "Üyeler",
|
||||||
"you": "Siz",
|
"you": "Siz",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"live": "Наживо",
|
"live": "Наживо",
|
||||||
"change_history": "Історія змін",
|
"change_history": "Історія змін",
|
||||||
"coming_soon": "Незабаром",
|
"coming_soon": "Незабаром",
|
||||||
|
"member": "Учасник",
|
||||||
"members": "Учасники",
|
"members": "Учасники",
|
||||||
"you": "Ви",
|
"you": "Ви",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -849,6 +849,7 @@
|
|||||||
"live": "Trực tiếp",
|
"live": "Trực tiếp",
|
||||||
"change_history": "Lịch sử thay đổi",
|
"change_history": "Lịch sử thay đổi",
|
||||||
"coming_soon": "Sắp ra mắt",
|
"coming_soon": "Sắp ra mắt",
|
||||||
|
"member": "Thành viên",
|
||||||
"members": "Thành viên",
|
"members": "Thành viên",
|
||||||
"you": "Bạn",
|
"you": "Bạn",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -849,6 +849,7 @@
|
|||||||
"live": "实时",
|
"live": "实时",
|
||||||
"change_history": "变更历史",
|
"change_history": "变更历史",
|
||||||
"coming_soon": "即将推出",
|
"coming_soon": "即将推出",
|
||||||
|
"member": "成员",
|
||||||
"members": "成员",
|
"members": "成员",
|
||||||
"you": "你",
|
"you": "你",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
@@ -850,6 +850,7 @@
|
|||||||
"live": "即時",
|
"live": "即時",
|
||||||
"change_history": "變更歷史記錄",
|
"change_history": "變更歷史記錄",
|
||||||
"coming_soon": "即將推出",
|
"coming_soon": "即將推出",
|
||||||
|
"member": "成員",
|
||||||
"members": "成員",
|
"members": "成員",
|
||||||
"you": "您",
|
"you": "您",
|
||||||
"upgrade_cta": {
|
"upgrade_cta": {
|
||||||
|
|||||||
46
packages/types/src/project/projects.d.ts
vendored
46
packages/types/src/project/projects.d.ts
vendored
@@ -1,14 +1,5 @@
|
|||||||
import { EUserProjectRoles } from "@plane/constants";
|
import { EUserProjectRoles } from "@plane/constants";
|
||||||
import type {
|
import type { IUser, IUserLite, IWorkspace, TLogoProps, TStateGroups } from "..";
|
||||||
IProjectViewProps,
|
|
||||||
IUser,
|
|
||||||
IUserLite,
|
|
||||||
IUserMemberLite,
|
|
||||||
IWorkspace,
|
|
||||||
IWorkspaceLite,
|
|
||||||
TLogoProps,
|
|
||||||
TStateGroups,
|
|
||||||
} from "..";
|
|
||||||
import { TUserPermissions } from "../enums";
|
import { TUserPermissions } from "../enums";
|
||||||
|
|
||||||
export interface IPartialProject {
|
export interface IPartialProject {
|
||||||
@@ -91,31 +82,20 @@ export interface IProjectMemberLite {
|
|||||||
member_id: string;
|
member_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProjectMember {
|
export type TProjectMembership = {
|
||||||
id: string;
|
|
||||||
member: IUserMemberLite;
|
|
||||||
project: IProjectLite;
|
|
||||||
workspace: IWorkspaceLite;
|
|
||||||
comment: string;
|
|
||||||
role: TUserPermissions;
|
|
||||||
|
|
||||||
preferences: ProjectPreferences;
|
|
||||||
|
|
||||||
view_props: IProjectViewProps;
|
|
||||||
default_props: IProjectViewProps;
|
|
||||||
|
|
||||||
created_at: Date;
|
|
||||||
updated_at: Date;
|
|
||||||
created_by: string;
|
|
||||||
updated_by: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IProjectMembership {
|
|
||||||
id: string;
|
|
||||||
member: string;
|
member: string;
|
||||||
role: TUserPermissions;
|
role: TUserPermissions | EUserProjectRoles;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
} & (
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
original_role: EUserProjectRoles;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: null;
|
||||||
|
original_role: null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export interface IProjectBulkAddFormData {
|
export interface IProjectBulkAddFormData {
|
||||||
members: { role: TUserPermissions | EUserProjectRoles; member_id: string }[];
|
members: { role: TUserPermissions | EUserProjectRoles; member_id: string }[];
|
||||||
|
|||||||
4
packages/types/src/workspace.d.ts
vendored
4
packages/types/src/workspace.d.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
import type { ICycle, IProjectMember, IUser, IUserLite, IWorkspaceViewProps, TPaginationInfo } from "@plane/types";
|
import type { ICycle, TProjectMembership, IUser, IUserLite, IWorkspaceViewProps, TPaginationInfo } from "@plane/types";
|
||||||
import { EUserWorkspaceRoles } from "@plane/constants"; // TODO: check if importing this over here causes circular dependency
|
import { EUserWorkspaceRoles } from "@plane/constants"; // TODO: check if importing this over here causes circular dependency
|
||||||
import { TUserPermissions } from "./enums";
|
import { TUserPermissions } from "./enums";
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ export interface IWorkspaceMemberMe {
|
|||||||
|
|
||||||
export interface ILastActiveWorkspaceDetails {
|
export interface ILastActiveWorkspaceDetails {
|
||||||
workspace_details: IWorkspace;
|
workspace_details: IWorkspace;
|
||||||
project_details?: IProjectMember[];
|
project_details?: TProjectMembership[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkspaceDefaultSearchResult {
|
export interface IWorkspaceDefaultSearchResult {
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ export * from "./emoji";
|
|||||||
export * from "./file";
|
export * from "./file";
|
||||||
export * from "./get-icon-for-link";
|
export * from "./get-icon-for-link";
|
||||||
export * from "./issue";
|
export * from "./issue";
|
||||||
|
export * from "./permission";
|
||||||
export * from "./state";
|
export * from "./state";
|
||||||
export * from "./string";
|
export * from "./string";
|
||||||
export * from "./subscription";
|
export * from "./subscription";
|
||||||
export * from "./theme";
|
export * from "./theme";
|
||||||
export * from "./work-item";
|
export * from "./work-item";
|
||||||
export * from "./workspace";
|
export * from "./workspace";
|
||||||
13
packages/utils/src/permission.ts
Normal file
13
packages/utils/src/permission.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { EUserPermissions, EUserProjectRoles, EUserWorkspaceRoles } from "@plane/constants";
|
||||||
|
|
||||||
|
type TSupportedRole = EUserPermissions | EUserProjectRoles | EUserWorkspaceRoles;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns the highest role from an array of supported roles
|
||||||
|
* @param { TSupportedRole[] } roles
|
||||||
|
* @returns { TSupportedRole | undefined }
|
||||||
|
*/
|
||||||
|
export const getHighestRole = <T extends TSupportedRole>(roles: T[]): T | undefined => {
|
||||||
|
if (!roles || roles.length === 0) return undefined;
|
||||||
|
return roles.reduce((highest, current) => (current > highest ? current : highest));
|
||||||
|
};
|
||||||
@@ -21,18 +21,17 @@ export interface IWorkspaceSettingLayout {
|
|||||||
const WorkspaceSettingLayout: FC<IWorkspaceSettingLayout> = observer((props) => {
|
const WorkspaceSettingLayout: FC<IWorkspaceSettingLayout> = observer((props) => {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { workspaceUserInfo } = useUserPermissions();
|
const { workspaceUserInfo, getWorkspaceRoleByWorkspaceSlug } = useUserPermissions();
|
||||||
// next hooks
|
// next hooks
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
// derived values
|
// derived values
|
||||||
const { workspaceSlug, accessKey } = pathnameToAccessKey(pathname);
|
const { workspaceSlug, accessKey } = pathnameToAccessKey(pathname);
|
||||||
const userWorkspaceRole = workspaceUserInfo?.[workspaceSlug.toString()]?.role;
|
const userWorkspaceRole = getWorkspaceRoleByWorkspaceSlug(workspaceSlug.toString());
|
||||||
|
|
||||||
const isAuthorized: boolean | string =
|
let isAuthorized: boolean | string = false;
|
||||||
pathname &&
|
if (pathname && workspaceSlug && userWorkspaceRole) {
|
||||||
workspaceSlug &&
|
isAuthorized = WORKSPACE_SETTINGS_ACCESS[accessKey]?.includes(userWorkspaceRole as EUserWorkspaceRoles);
|
||||||
userWorkspaceRole &&
|
}
|
||||||
WORKSPACE_SETTINGS_ACCESS[accessKey]?.includes(userWorkspaceRole as EUserWorkspaceRoles);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// components
|
import { useParams } from "next/navigation";
|
||||||
|
// plane imports
|
||||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
// components
|
||||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||||
import { PageHead } from "@/components/core";
|
import { PageHead } from "@/components/core";
|
||||||
import { ProjectMemberList, ProjectSettingsMemberDefaults } from "@/components/project";
|
import { ProjectMemberList, ProjectSettingsMemberDefaults } from "@/components/project";
|
||||||
// hooks
|
// hooks
|
||||||
import { SettingsContentWrapper } from "@/components/settings";
|
import { SettingsContentWrapper } from "@/components/settings";
|
||||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||||
|
// plane web imports
|
||||||
|
import { ProjectTeamspaceList } from "@/plane-web/components/projects/teamspaces";
|
||||||
|
import { getProjectSettingsPageLabelI18nKey } from "@/plane-web/helpers/project-settings";
|
||||||
|
|
||||||
const MembersSettingsPage = observer(() => {
|
const MembersSettingsPage = observer(() => {
|
||||||
// store
|
// router
|
||||||
|
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams();
|
||||||
|
// plane hooks
|
||||||
|
const { t } = useTranslation();
|
||||||
|
// store hooks
|
||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||||
// derived values
|
// derived values
|
||||||
|
const projectId = routerProjectId?.toString();
|
||||||
|
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined;
|
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined;
|
||||||
const isProjectMemberOrAdmin = allowPermissions(
|
const isProjectMemberOrAdmin = allowPermissions(
|
||||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||||
@@ -31,8 +43,14 @@ const MembersSettingsPage = observer(() => {
|
|||||||
<SettingsContentWrapper size="lg">
|
<SettingsContentWrapper size="lg">
|
||||||
<PageHead title={pageTitle} />
|
<PageHead title={pageTitle} />
|
||||||
<section className={`w-full`}>
|
<section className={`w-full`}>
|
||||||
<ProjectSettingsMemberDefaults />
|
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
||||||
<ProjectMemberList />
|
<div className="text-lg font-semibold">
|
||||||
|
{t(getProjectSettingsPageLabelI18nKey("members", "common.members"))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ProjectSettingsMemberDefaults projectId={projectId} workspaceSlug={workspaceSlug} />
|
||||||
|
<ProjectTeamspaceList projectId={projectId} workspaceSlug={workspaceSlug} />
|
||||||
|
<ProjectMemberList projectId={projectId} workspaceSlug={workspaceSlug} />
|
||||||
</section>
|
</section>
|
||||||
</SettingsContentWrapper>
|
</SettingsContentWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ const OnboardingPage = observer(() => {
|
|||||||
await finishUserOnboarding()
|
await finishUserOnboarding()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
captureEvent(USER_ONBOARDING_COMPLETED, {
|
captureEvent(USER_ONBOARDING_COMPLETED, {
|
||||||
// user_role: user.role,
|
|
||||||
email: user.email,
|
email: user.email,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
status: "SUCCESS",
|
status: "SUCCESS",
|
||||||
|
|||||||
@@ -1,34 +1,28 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams } from "next/navigation";
|
// plane imports
|
||||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||||
import { IWorkspaceMember } from "@plane/types";
|
import { IWorkspaceMember, TProjectMembership } from "@plane/types";
|
||||||
|
// components
|
||||||
import { AccountTypeColumn, NameColumn } from "@/components/project/settings/member-columns";
|
import { AccountTypeColumn, NameColumn } from "@/components/project/settings/member-columns";
|
||||||
|
// hooks
|
||||||
import { useUser, useUserPermissions } from "@/hooks/store";
|
import { useUser, useUserPermissions } from "@/hooks/store";
|
||||||
|
|
||||||
export interface RowData {
|
export interface RowData extends Pick<TProjectMembership, "original_role"> {
|
||||||
member: IWorkspaceMember;
|
member: IWorkspaceMember;
|
||||||
role: EUserPermissions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useProjectColumns = () => {
|
type TUseProjectColumnsProps = {
|
||||||
|
projectId: string;
|
||||||
|
workspaceSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProjectColumns = (props: TUseProjectColumnsProps) => {
|
||||||
|
const { projectId, workspaceSlug } = props;
|
||||||
// states
|
// states
|
||||||
const [removeMemberModal, setRemoveMemberModal] = useState<RowData | null>(null);
|
const [removeMemberModal, setRemoveMemberModal] = useState<RowData | null>(null);
|
||||||
|
// store hooks
|
||||||
const { workspaceSlug, projectId } = useParams();
|
|
||||||
|
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
const { allowPermissions, projectUserInfo } = useUserPermissions();
|
const { allowPermissions, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||||||
|
|
||||||
const currentProjectRole =
|
|
||||||
(projectUserInfo?.[workspaceSlug.toString()]?.[projectId.toString()]?.role as unknown as EUserPermissions) ??
|
|
||||||
EUserPermissions.GUEST;
|
|
||||||
|
|
||||||
const getFormattedDate = (dateStr: string) => {
|
|
||||||
const date = new Date(dateStr);
|
|
||||||
|
|
||||||
const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", day: "numeric" };
|
|
||||||
return date.toLocaleDateString("en-US", options);
|
|
||||||
};
|
|
||||||
// derived values
|
// derived values
|
||||||
const isAdmin = allowPermissions(
|
const isAdmin = allowPermissions(
|
||||||
[EUserPermissions.ADMIN],
|
[EUserPermissions.ADMIN],
|
||||||
@@ -36,6 +30,15 @@ export const useProjectColumns = () => {
|
|||||||
workspaceSlug.toString(),
|
workspaceSlug.toString(),
|
||||||
projectId.toString()
|
projectId.toString()
|
||||||
);
|
);
|
||||||
|
const currentProjectRole =
|
||||||
|
getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug.toString(), projectId.toString()) ?? EUserPermissions.GUEST;
|
||||||
|
|
||||||
|
const getFormattedDate = (dateStr: string) => {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
|
||||||
|
const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", day: "numeric" };
|
||||||
|
return date.toLocaleDateString("en-US", options);
|
||||||
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@@ -76,5 +79,5 @@ export const useProjectColumns = () => {
|
|||||||
tdRender: (rowData: RowData) => <div>{getFormattedDate(rowData?.member?.joining_date || "")}</div>,
|
tdRender: (rowData: RowData) => <div>{getFormattedDate(rowData?.member?.joining_date || "")}</div>,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return { columns, workspaceSlug, projectId, removeMemberModal, setRemoveMemberModal };
|
return { columns, removeMemberModal, setRemoveMemberModal };
|
||||||
};
|
};
|
||||||
|
|||||||
1
web/ce/components/projects/teamspaces/index.ts
Normal file
1
web/ce/components/projects/teamspaces/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./teamspace-list";
|
||||||
6
web/ce/components/projects/teamspaces/teamspace-list.tsx
Normal file
6
web/ce/components/projects/teamspaces/teamspace-list.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export type TProjectTeamspaceList = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectTeamspaceList: React.FC<TProjectTeamspaceList> = () => null;
|
||||||
@@ -16,7 +16,7 @@ export const PROJECT_SETTINGS = {
|
|||||||
},
|
},
|
||||||
members: {
|
members: {
|
||||||
key: "members",
|
key: "members",
|
||||||
i18n_label: "members",
|
i18n_label: "common.members",
|
||||||
href: `/members`,
|
href: `/members`,
|
||||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/members/`,
|
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/members/`,
|
||||||
|
|||||||
7
web/ce/helpers/project-settings.ts
Normal file
7
web/ce/helpers/project-settings.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @description Get the i18n key for the project settings page label
|
||||||
|
* @param _settingsKey - The key of the project settings page
|
||||||
|
* @param defaultLabelKey - The default i18n key for the project settings page label
|
||||||
|
* @returns The i18n key for the project settings page label
|
||||||
|
*/
|
||||||
|
export const getProjectSettingsPageLabelI18nKey = (_settingsKey: string, defaultLabelKey: string) => defaultLabelKey;
|
||||||
43
web/ce/store/member/project-member.store.ts
Normal file
43
web/ce/store/member/project-member.store.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { computedFn } from "mobx-utils";
|
||||||
|
// plane imports
|
||||||
|
import { EUserProjectRoles } from "@plane/constants";
|
||||||
|
// plane web imports
|
||||||
|
import { RootStore } from "@/plane-web/store/root.store";
|
||||||
|
// store
|
||||||
|
import { IMemberRootStore } from "@/store/member";
|
||||||
|
import { BaseProjectMemberStore, IBaseProjectMemberStore } from "@/store/member/base-project-member.store";
|
||||||
|
|
||||||
|
export type IProjectMemberStore = IBaseProjectMemberStore;
|
||||||
|
|
||||||
|
export class ProjectMemberStore extends BaseProjectMemberStore implements IProjectMemberStore {
|
||||||
|
constructor(_memberRoot: IMemberRootStore, rootStore: RootStore) {
|
||||||
|
super(_memberRoot, rootStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns the highest role from the project membership
|
||||||
|
* @param { string } userId
|
||||||
|
* @param { string } projectId
|
||||||
|
* @returns { EUserProjectRoles | undefined }
|
||||||
|
*/
|
||||||
|
getUserProjectRole = computedFn((userId: string, projectId: string): EUserProjectRoles | undefined =>
|
||||||
|
this.getRoleFromProjectMembership(userId, projectId)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns the role from the project membership
|
||||||
|
* @param projectId
|
||||||
|
* @param userId
|
||||||
|
* @param role
|
||||||
|
*/
|
||||||
|
getProjectMemberRoleForUpdate = (_projectId: string, _userId: string, role: EUserProjectRoles): EUserProjectRoles =>
|
||||||
|
role;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Processes the removal of a member from a project
|
||||||
|
* This method handles the cleanup of member data from the project member map
|
||||||
|
* @param projectId - The ID of the project to remove the member from
|
||||||
|
* @param userId - The ID of the user to remove from the project
|
||||||
|
*/
|
||||||
|
processMemberRemoval = (projectId: string, userId: string) => this.handleMemberRemoval(projectId, userId);
|
||||||
|
}
|
||||||
23
web/ce/store/user/permission.store.ts
Normal file
23
web/ce/store/user/permission.store.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { computedFn } from "mobx-utils";
|
||||||
|
import { EUserPermissions } from "@plane/constants";
|
||||||
|
import { RootStore } from "@/plane-web/store/root.store";
|
||||||
|
import { BaseUserPermissionStore, IBaseUserPermissionStore } from "@/store/user/base-permissions.store";
|
||||||
|
|
||||||
|
export type IUserPermissionStore = IBaseUserPermissionStore;
|
||||||
|
|
||||||
|
export class UserPermissionStore extends BaseUserPermissionStore implements IUserPermissionStore {
|
||||||
|
constructor(store: RootStore) {
|
||||||
|
super(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns the project role from the workspace
|
||||||
|
* @param { string } workspaceSlug
|
||||||
|
* @param { string } projectId
|
||||||
|
* @returns { EUserPermissions | undefined }
|
||||||
|
*/
|
||||||
|
getProjectRoleByWorkspaceSlugAndProjectId = computedFn(
|
||||||
|
(workspaceSlug: string, projectId: string): EUserPermissions | undefined =>
|
||||||
|
this.getProjectRole(workspaceSlug, projectId)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,23 +2,28 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
// hooks
|
|
||||||
import { ClipboardList } from "lucide-react";
|
import { ClipboardList } from "lucide-react";
|
||||||
|
// plane imports
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||||
// ui
|
// assets
|
||||||
// icons
|
import Unauthorized from "@/public/auth/unauthorized.svg";
|
||||||
// images
|
|
||||||
import JoinProjectImg from "@/public/auth/project-not-authorized.svg";
|
|
||||||
|
|
||||||
export const JoinProject: React.FC = () => {
|
type Props = {
|
||||||
|
projectId?: string;
|
||||||
|
isPrivateProject?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const JoinProject: React.FC<Props> = (props) => {
|
||||||
|
const { projectId, isPrivateProject = false } = props;
|
||||||
// states
|
// states
|
||||||
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
const [isJoiningProject, setIsJoiningProject] = useState(false);
|
||||||
// store hooks
|
// store hooks
|
||||||
const { joinProject } = useUserPermissions();
|
const { joinProject } = useUserPermissions();
|
||||||
const { fetchProjectDetails } = useProject();
|
const { fetchProjectDetails } = useProject();
|
||||||
|
|
||||||
const { workspaceSlug, projectId } = useParams();
|
const { workspaceSlug } = useParams();
|
||||||
|
|
||||||
const handleJoin = () => {
|
const handleJoin = () => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
@@ -33,25 +38,31 @@ export const JoinProject: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-custom-background-100 text-center">
|
<div className="flex h-full w-full flex-col items-center justify-center gap-y-5 bg-custom-background-100 text-center">
|
||||||
<div className="h-44 w-72">
|
<div className="h-44 w-72">
|
||||||
<Image src={JoinProjectImg} height="176" width="288" alt="JoinProject" />
|
<Image src={Unauthorized} height="176" width="288" alt="JoinProject" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-xl font-medium text-custom-text-100">You are not a member of this project</h1>
|
<h1 className="text-xl font-medium text-custom-text-100">
|
||||||
|
{!isPrivateProject ? `You are not a member of this project yet.` : `You are not a member of this project.`}
|
||||||
|
</h1>
|
||||||
|
|
||||||
<div className="w-full max-w-md text-base text-custom-text-200">
|
<div className="w-full max-w-md text-base text-custom-text-200">
|
||||||
<p className="mx-auto w-full text-sm md:w-3/4">
|
<p className="mx-auto w-full text-sm md:w-3/4">
|
||||||
You are not a member of this project, but you can join this project by clicking the button below.
|
{!isPrivateProject
|
||||||
|
? `Click the button below to join it.`
|
||||||
|
: `This is a private project. \n We can't tell you more about this project to protect confidentiality.`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{!isPrivateProject && (
|
||||||
<Button
|
<div>
|
||||||
variant="primary"
|
<Button
|
||||||
prependIcon={<ClipboardList color="white" />}
|
variant="primary"
|
||||||
loading={isJoiningProject}
|
prependIcon={<ClipboardList color="white" />}
|
||||||
onClick={handleJoin}
|
loading={isJoiningProject}
|
||||||
>
|
onClick={handleJoin}
|
||||||
{isJoiningProject ? "Joining..." : "Click to join"}
|
>
|
||||||
</Button>
|
{isJoiningProject ? "Taking you in" : "Click to join"}
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
|||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
const { currentTab, fetchInboxIssueById, getIssueInboxByIssueId, getIsIssueAvailable } = useProjectInbox();
|
const { currentTab, fetchInboxIssueById, getIssueInboxByIssueId, getIsIssueAvailable } = useProjectInbox();
|
||||||
const inboxIssue = getIssueInboxByIssueId(inboxIssueId);
|
const inboxIssue = getIssueInboxByIssueId(inboxIssueId);
|
||||||
const { allowPermissions, projectPermissionsByWorkspaceSlugAndProjectId } = useUserPermissions();
|
const { allowPermissions, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || "");
|
const isIssueAvailable = getIsIssueAvailable(inboxIssueId?.toString() || "");
|
||||||
@@ -67,7 +67,7 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
|||||||
allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId) ||
|
allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId) ||
|
||||||
inboxIssue?.issue.created_by === currentUser?.id;
|
inboxIssue?.issue.created_by === currentUser?.id;
|
||||||
|
|
||||||
const isGuest = projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId) === EUserPermissions.GUEST;
|
const isGuest = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId) === EUserPermissions.GUEST;
|
||||||
const isOwner = inboxIssue?.issue.created_by === currentUser?.id;
|
const isOwner = inboxIssue?.issue.created_by === currentUser?.id;
|
||||||
const readOnly = !isOwner && isGuest;
|
const readOnly = !isOwner && isGuest;
|
||||||
|
|
||||||
|
|||||||
@@ -49,14 +49,14 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
|||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
|
||||||
const { projectPermissionsByWorkspaceSlugAndProjectId } = useUserPermissions();
|
const { getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
// derived values
|
// derived values
|
||||||
const issue = issueId ? getIssueById(issueId) : undefined;
|
const issue = issueId ? getIssueById(issueId) : undefined;
|
||||||
const currentUserProjectRole = projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
|
const currentUserProjectRole = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
|
||||||
const isAdmin = (currentUserProjectRole ?? EUserPermissions.GUEST) === EUserPermissions.ADMIN;
|
const isAdmin = currentUserProjectRole === EUserPermissions.ADMIN;
|
||||||
const isGuest = (currentUserProjectRole ?? EUserPermissions.GUEST) === EUserPermissions.GUEST;
|
const isGuest = currentUserProjectRole === EUserPermissions.GUEST;
|
||||||
const isAssigned = issue?.assignee_ids && currentUser?.id ? issue?.assignee_ids.includes(currentUser?.id) : false;
|
const isAssigned = issue?.assignee_ids && currentUser?.id ? issue?.assignee_ids.includes(currentUser?.id) : false;
|
||||||
const isWorklogButtonEnabled = !isIntakeIssue && !isGuest && (isAdmin || isAssigned);
|
const isWorklogButtonEnabled = !isIntakeIssue && !isGuest && (isAdmin || isAssigned);
|
||||||
// toggle filter
|
// toggle filter
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ const ProjectCreateHeader: React.FC<Props> = (props) => {
|
|||||||
alt={t("project_cover_image_alt")}
|
alt={t("project_cover_image_alt")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="absolute left-2.5 top-2.5">
|
<div className="absolute left-2.5 top-2.5">
|
||||||
<ProjectTemplateSelect handleModalClose={handleClose} />
|
<ProjectTemplateSelect handleModalClose={handleClose} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const ProjectCreateButtons: React.FC<Props> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex justify-end gap-2 py-4 border-t border-custom-border-100">
|
<div className="flex justify-end gap-2 py-4 border-t border-custom-border-100">
|
||||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={getIndex("cancel")}>
|
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={getIndex("cancel")}>
|
||||||
{t("cancel")}
|
{t("common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={getIndex("submit")}>
|
<Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={getIndex("submit")}>
|
||||||
{isSubmitting ? t("creating") : t("create_project")}
|
{isSubmitting ? t("creating") : t("create_project")}
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
// plane imports
|
||||||
import { PROJECT_MEMBER_LEAVE } from "@plane/constants";
|
import { PROJECT_MEMBER_LEAVE } from "@plane/constants";
|
||||||
import { TOAST_TYPE, Table, setToast } from "@plane/ui";
|
import { TOAST_TYPE, Table, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { ConfirmProjectMemberRemove } from "@/components/project";
|
import { ConfirmProjectMemberRemove } from "@/components/project";
|
||||||
// constants
|
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useMember, useUser, useUserPermissions } from "@/hooks/store";
|
import { useEventTracker, useMember, useUser, useUserPermissions } from "@/hooks/store";
|
||||||
import { useAppRouter } from "@/hooks/use-app-router";
|
import { useAppRouter } from "@/hooks/use-app-router";
|
||||||
|
// plane web imports
|
||||||
import { useProjectColumns } from "@/plane-web/components/projects/settings/useProjectColumns";
|
import { useProjectColumns } from "@/plane-web/components/projects/settings/useProjectColumns";
|
||||||
import { IProjectMemberDetails } from "@/store/member/project-member.store";
|
// store
|
||||||
|
import { IProjectMemberDetails } from "@/store/member/base-project-member.store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
memberDetails: (IProjectMemberDetails | null)[];
|
memberDetails: (IProjectMemberDetails | null)[];
|
||||||
|
projectId: string;
|
||||||
|
workspaceSlug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
||||||
const { memberDetails } = props;
|
const { memberDetails, projectId, workspaceSlug } = props;
|
||||||
const { columns, workspaceSlug, projectId, removeMemberModal, setRemoveMemberModal } = useProjectColumns();
|
|
||||||
|
|
||||||
// router
|
// router
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
// store hooks
|
// store hooks
|
||||||
@@ -31,7 +31,11 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||||||
project: { removeMemberFromProject },
|
project: { removeMemberFromProject },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
// const { isMobile } = usePlatformOS();
|
// helper hooks
|
||||||
|
const { columns, removeMemberModal, setRemoveMemberModal } = useProjectColumns({
|
||||||
|
projectId,
|
||||||
|
workspaceSlug,
|
||||||
|
});
|
||||||
|
|
||||||
const handleRemove = async (memberId: string) => {
|
const handleRemove = async (memberId: string) => {
|
||||||
if (!workspaceSlug || !projectId || !memberId) return;
|
if (!workspaceSlug || !projectId || !memberId) return;
|
||||||
|
|||||||
@@ -2,22 +2,26 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
// hooks
|
// plane imports
|
||||||
// components
|
|
||||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
|
// components
|
||||||
import { ProjectMemberListItem, SendProjectInvitationModal } from "@/components/project";
|
import { ProjectMemberListItem, SendProjectInvitationModal } from "@/components/project";
|
||||||
// ui
|
|
||||||
import { MembersSettingsLoader } from "@/components/ui";
|
import { MembersSettingsLoader } from "@/components/ui";
|
||||||
|
// hooks
|
||||||
import { useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
|
import { useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
|
||||||
|
import { getProjectSettingsPageLabelI18nKey } from "@/plane-web/helpers/project-settings";
|
||||||
import { SettingsHeading } from "../settings";
|
import { SettingsHeading } from "../settings";
|
||||||
|
|
||||||
export const ProjectMemberList: React.FC = observer(() => {
|
type TProjectMemberListProps = {
|
||||||
// router
|
projectId: string;
|
||||||
const { projectId } = useParams();
|
workspaceSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectMemberList: React.FC<TProjectMemberListProps> = observer((props) => {
|
||||||
|
const { projectId, workspaceSlug } = props;
|
||||||
// states
|
// states
|
||||||
const [inviteModal, setInviteModal] = useState(false);
|
const [inviteModal, setInviteModal] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
@@ -30,17 +34,17 @@ export const ProjectMemberList: React.FC = observer(() => {
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const searchedMembers = (projectMemberIds ?? []).filter((userId) => {
|
const searchedProjectMembers = (projectMemberIds ?? []).filter((userId) => {
|
||||||
const memberDetails = projectId ? getProjectMemberDetails(userId, projectId.toString()) : null;
|
const memberDetails = projectId ? getProjectMemberDetails(userId, projectId.toString()) : null;
|
||||||
|
|
||||||
if (!memberDetails?.member) return false;
|
if (!memberDetails?.member || !memberDetails.original_role) return false;
|
||||||
|
|
||||||
const fullName = `${memberDetails?.member.first_name} ${memberDetails?.member.last_name}`.toLowerCase();
|
const fullName = `${memberDetails?.member.first_name} ${memberDetails?.member.last_name}`.toLowerCase();
|
||||||
const displayName = memberDetails?.member.display_name.toLowerCase();
|
const displayName = memberDetails?.member.display_name.toLowerCase();
|
||||||
|
|
||||||
return displayName?.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase());
|
return displayName?.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase());
|
||||||
});
|
});
|
||||||
const memberDetails = searchedMembers?.map((memberId) =>
|
const memberDetails = searchedProjectMembers?.map((memberId) =>
|
||||||
projectId ? getProjectMemberDetails(memberId, projectId.toString()) : null
|
projectId ? getProjectMemberDetails(memberId, projectId.toString()) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -48,9 +52,14 @@ export const ProjectMemberList: React.FC = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SendProjectInvitationModal isOpen={inviteModal} onClose={() => setInviteModal(false)} />
|
<SendProjectInvitationModal
|
||||||
|
isOpen={inviteModal}
|
||||||
|
onClose={() => setInviteModal(false)}
|
||||||
|
projectId={projectId}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
<SettingsHeading
|
<SettingsHeading
|
||||||
title={t("members")}
|
title={t(getProjectSettingsPageLabelI18nKey("members", "common.members"))}
|
||||||
appendToRight={
|
appendToRight={
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="ml-auto flex items-center justify-start gap-1 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5">
|
<div className="ml-auto flex items-center justify-start gap-1 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5">
|
||||||
@@ -82,9 +91,14 @@ export const ProjectMemberList: React.FC = observer(() => {
|
|||||||
<MembersSettingsLoader />
|
<MembersSettingsLoader />
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-custom-border-100 overflow-scroll">
|
<div className="divide-y divide-custom-border-100 overflow-scroll">
|
||||||
{searchedMembers.length !== 0 && <ProjectMemberListItem memberDetails={memberDetails ?? []} />}
|
{searchedProjectMembers.length !== 0 && (
|
||||||
|
<ProjectMemberListItem
|
||||||
{searchedMembers.length === 0 && (
|
memberDetails={memberDetails ?? []}
|
||||||
|
projectId={projectId}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{searchedProjectMembers.length === 0 && (
|
||||||
<h4 className="text-sm mt-16 text-center text-custom-text-400">{t("no_matching_members")}</h4>
|
<h4 className="text-sm mt-16 text-center text-custom-text-400">{t("no_matching_members")}</h4>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
|||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { Ban } from "lucide-react";
|
import { Ban } from "lucide-react";
|
||||||
// plane ui
|
// plane ui
|
||||||
import { EUserPermissions } from "@plane/constants";
|
import { EUserProjectRoles } from "@plane/constants";
|
||||||
import { Avatar, CustomSearchSelect } from "@plane/ui";
|
import { Avatar, CustomSearchSelect } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
import { getFileURL } from "@/helpers/file.helper";
|
||||||
@@ -32,7 +32,7 @@ export const MemberSelect: React.FC<Props> = observer((props) => {
|
|||||||
const memberDetails = projectId ? getProjectMemberDetails(userId, projectId.toString()) : null;
|
const memberDetails = projectId ? getProjectMemberDetails(userId, projectId.toString()) : null;
|
||||||
|
|
||||||
if (!memberDetails?.member) return;
|
if (!memberDetails?.member) return;
|
||||||
const isGuest = memberDetails.role === EUserPermissions.GUEST;
|
const isGuest = memberDetails.role === EUserProjectRoles.GUEST;
|
||||||
if (isGuest) return;
|
if (isGuest) return;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -59,7 +59,7 @@ export const MemberSelect: React.FC<Props> = observer((props) => {
|
|||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
value={value}
|
value={value}
|
||||||
label={
|
label={
|
||||||
<div className="flex items-center gap-2 h-5">
|
<div className="flex items-center gap-2 h-3.5">
|
||||||
{selectedOption && (
|
{selectedOption && (
|
||||||
<Avatar name={selectedOption.member?.display_name} src={getFileURL(selectedOption.member?.avatar_url)} />
|
<Avatar name={selectedOption.member?.display_name} src={getFileURL(selectedOption.member?.avatar_url)} />
|
||||||
)}
|
)}
|
||||||
@@ -73,7 +73,7 @@ export const MemberSelect: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonClassName="!px-3 !py-2"
|
buttonClassName="!px-3 !py-2 bg-custom-background-100"
|
||||||
options={
|
options={
|
||||||
options &&
|
options &&
|
||||||
options && [
|
options && [
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export const ProjectMultiSelectModal: React.FC<Props> = observer((props) => {
|
|||||||
</Combobox>
|
</Combobox>
|
||||||
<div className="flex items-center justify-end gap-2 p-3 border-t border-custom-border-100">
|
<div className="flex items-center justify-end gap-2 p-3 border-t border-custom-border-100">
|
||||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||||
Cancel
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
ref={moveButtonRef}
|
ref={moveButtonRef}
|
||||||
@@ -176,7 +176,7 @@ export const ProjectMultiSelectModal: React.FC<Props> = observer((props) => {
|
|||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={!areSelectedProjectsChanged}
|
disabled={!areSelectedProjectsChanged}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Confirming" : "Confirm"}
|
{isSubmitting ? t("confirming") : t("confirm")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalCore>
|
</ModalCore>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect, ReactNode } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
// plane imports
|
||||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { IProject, IUserLite, IWorkspace } from "@plane/types";
|
import { IProject, IUserLite, IWorkspace } from "@plane/types";
|
||||||
// ui
|
|
||||||
import { Loader, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
|
import { Loader, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { MemberSelect } from "@/components/project";
|
import { MemberSelect } from "@/components/project";
|
||||||
@@ -17,35 +16,53 @@ import { PROJECT_MEMBERS } from "@/constants/fetch-keys";
|
|||||||
// hooks
|
// hooks
|
||||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||||
|
|
||||||
// types
|
|
||||||
|
|
||||||
const defaultValues: Partial<IProject> = {
|
const defaultValues: Partial<IProject> = {
|
||||||
project_lead: null,
|
project_lead: null,
|
||||||
default_assignee: null,
|
default_assignee: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
|
type TDefaultSettingItemProps = {
|
||||||
// router
|
title: string;
|
||||||
const { workspaceSlug, projectId } = useParams();
|
description: string;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultSettingItem: React.FC<TDefaultSettingItemProps> = ({ title, description, children }) => (
|
||||||
|
<div className="flex items-center justify-between gap-x-2">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<h4 className="text-sm font-medium">{title}</h4>
|
||||||
|
<p className="text-xs text-custom-text-300">{description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-full max-w-48 sm:max-w-64">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
type TProjectSettingsMemberDefaultsProps = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectSettingsMemberDefaults: React.FC<TProjectSettingsMemberDefaultsProps> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId } = props;
|
||||||
|
// plane hooks
|
||||||
|
const { t } = useTranslation();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const { currentProjectDetails, fetchProjectDetails, updateProject } = useProject();
|
const { currentProjectDetails, fetchProjectDetails, updateProject } = useProject();
|
||||||
// derived values
|
// derived values
|
||||||
const isAdmin = allowPermissions(
|
const isAdmin = allowPermissions(
|
||||||
[EUserPermissions.ADMIN],
|
[EUserPermissions.ADMIN],
|
||||||
EUserPermissionsLevel.PROJECT,
|
EUserPermissionsLevel.PROJECT,
|
||||||
workspaceSlug?.toString(),
|
workspaceSlug,
|
||||||
currentProjectDetails?.id
|
currentProjectDetails?.id
|
||||||
);
|
);
|
||||||
// form info
|
// form info
|
||||||
const { reset, control } = useForm<IProject>({ defaultValues });
|
const { reset, control } = useForm<IProject>({ defaultValues });
|
||||||
// fetching user members
|
// fetching user members
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId.toString()) : null,
|
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId) : null,
|
||||||
workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null
|
workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug, projectId) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -71,7 +88,7 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
|
|||||||
...formData,
|
...formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateProject(workspaceSlug.toString(), projectId.toString(), {
|
await updateProject(workspaceSlug, projectId, {
|
||||||
default_assignee:
|
default_assignee:
|
||||||
formData.default_assignee === "none"
|
formData.default_assignee === "none"
|
||||||
? null
|
? null
|
||||||
@@ -94,7 +111,7 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
|
|||||||
const toggleGuestViewAllIssues = async (value: boolean) => {
|
const toggleGuestViewAllIssues = async (value: boolean) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
updateProject(workspaceSlug.toString(), projectId.toString(), {
|
updateProject(workspaceSlug, projectId, {
|
||||||
guest_view_all_features: value,
|
guest_view_all_features: value,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -110,82 +127,64 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-y-6 my-6">
|
||||||
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
<DefaultSettingItem title="Project Lead" description="Select the project lead for the project.">
|
||||||
<h3 className="text-xl font-medium">{t("common.defaults")}</h3>
|
{currentProjectDetails ? (
|
||||||
</div>
|
<Controller
|
||||||
|
control={control}
|
||||||
<div className="flex w-full flex-col gap-2 pb-4">
|
name="project_lead"
|
||||||
<div className="flex w-full items-center gap-4 py-4">
|
render={({ field: { value } }) => (
|
||||||
<div className="flex w-1/2 flex-col gap-2">
|
<MemberSelect
|
||||||
<h4 className="text-sm">{t("project_settings.members.project_lead")}</h4>
|
value={value}
|
||||||
<div className="">
|
onChange={(val: string) => {
|
||||||
{currentProjectDetails ? (
|
submitChanges({ project_lead: val });
|
||||||
<Controller
|
}}
|
||||||
control={control}
|
isDisabled={!isAdmin}
|
||||||
name="project_lead"
|
/>
|
||||||
render={({ field: { value } }) => (
|
)}
|
||||||
<MemberSelect
|
|
||||||
value={value}
|
|
||||||
onChange={(val: string) => {
|
|
||||||
submitChanges({ project_lead: val });
|
|
||||||
}}
|
|
||||||
isDisabled={!isAdmin}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Loader className="h-9 w-full">
|
|
||||||
<Loader.Item width="100%" height="100%" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-1/2 flex-col gap-2">
|
|
||||||
<h4 className="text-sm">{t("project_settings.members.default_assignee")}</h4>
|
|
||||||
<div className="">
|
|
||||||
{currentProjectDetails ? (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="default_assignee"
|
|
||||||
render={({ field: { value } }) => (
|
|
||||||
<MemberSelect
|
|
||||||
value={value}
|
|
||||||
onChange={(val: string) => {
|
|
||||||
submitChanges({ default_assignee: val });
|
|
||||||
}}
|
|
||||||
isDisabled={!isAdmin}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Loader className="h-9 w-full">
|
|
||||||
<Loader.Item width="100%" height="100%" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{currentProjectDetails && (
|
|
||||||
<div className="relative pb-4 flex justify-between items-center gap-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<h3 className="text-lg font-medium text-custom-text-100">
|
|
||||||
{t("project_settings.members.guest_super_permissions.title")}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-custom-text-200">
|
|
||||||
{t("project_settings.members.guest_super_permissions.sub_heading")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ToggleSwitch
|
|
||||||
value={!!currentProjectDetails?.guest_view_all_features}
|
|
||||||
onChange={() => toggleGuestViewAllIssues(!currentProjectDetails?.guest_view_all_features)}
|
|
||||||
disabled={!isAdmin}
|
|
||||||
size="md"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
) : (
|
||||||
|
<Loader className="h-9 w-full">
|
||||||
|
<Loader.Item width="100%" height="100%" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</DefaultSettingItem>
|
||||||
|
<DefaultSettingItem title="Default Assignee" description="Select the default assignee for the project.">
|
||||||
|
{currentProjectDetails ? (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="default_assignee"
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<MemberSelect
|
||||||
|
value={value}
|
||||||
|
onChange={(val: string) => {
|
||||||
|
submitChanges({ default_assignee: val });
|
||||||
|
}}
|
||||||
|
isDisabled={!isAdmin}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Loader className="h-9 w-full">
|
||||||
|
<Loader.Item width="100%" height="100%" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</DefaultSettingItem>
|
||||||
|
{currentProjectDetails && (
|
||||||
|
<DefaultSettingItem
|
||||||
|
title="Guest access"
|
||||||
|
description="This will allow guests to have view access to all the project work items."
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<ToggleSwitch
|
||||||
|
value={!!currentProjectDetails?.guest_view_all_features}
|
||||||
|
onChange={() => toggleGuestViewAllIssues(!currentProjectDetails?.guest_view_all_features)}
|
||||||
|
disabled={!isAdmin}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DefaultSettingItem>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,26 +2,24 @@
|
|||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { useForm, Controller, useFieldArray } from "react-hook-form";
|
import { useForm, Controller, useFieldArray } from "react-hook-form";
|
||||||
import { ChevronDown, Plus, X } from "lucide-react";
|
import { ChevronDown, Plus, X } from "lucide-react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { ROLE, PROJECT_MEMBER_ADDED, EUserPermissions } from "@plane/constants";
|
import { ROLE, PROJECT_MEMBER_ADDED, EUserPermissions } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
// ui
|
|
||||||
import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui";
|
import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// constants
|
|
||||||
// helpers
|
// helpers
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
import { getFileURL } from "@/helpers/file.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
|
import { useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
|
||||||
// plane-web constants
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
|
projectId: string;
|
||||||
|
workspaceSlug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type member = {
|
type member = {
|
||||||
@@ -43,14 +41,14 @@ const defaultValues: FormValues = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
||||||
const { isOpen, onClose, onSuccess } = props;
|
const { isOpen, onClose, onSuccess, projectId, workspaceSlug } = props;
|
||||||
// router
|
// plane hooks
|
||||||
const { workspaceSlug, projectId } = useParams();
|
const { t } = useTranslation();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
const { projectUserInfo } = useUserPermissions();
|
const { getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||||||
const {
|
const {
|
||||||
project: { projectMemberIds, bulkAddMembersToProject },
|
project: { getProjectMemberDetails, bulkAddMembersToProject },
|
||||||
workspace: { workspaceMemberIds, getWorkspaceMemberDetails },
|
workspace: { workspaceMemberIds, getWorkspaceMemberDetails },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
// form info
|
// form info
|
||||||
@@ -62,19 +60,15 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
} = useForm<FormValues>();
|
} = useForm<FormValues>();
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
name: "members",
|
name: "members",
|
||||||
});
|
});
|
||||||
|
// derived values
|
||||||
const { t } = useTranslation();
|
const currentProjectRole = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
|
||||||
|
|
||||||
const currentProjectRole = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]?.role;
|
|
||||||
|
|
||||||
const uninvitedPeople = workspaceMemberIds?.filter((userId) => {
|
const uninvitedPeople = workspaceMemberIds?.filter((userId) => {
|
||||||
const isInvited = projectMemberIds?.find((u) => u === userId);
|
const projectMemberDetails = getProjectMemberDetails(userId, projectId);
|
||||||
|
const isInvited = projectMemberDetails?.member.id && projectMemberDetails?.original_role;
|
||||||
return !isInvited;
|
return !isInvited;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,7 +175,6 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||||||
currentMemberWorkspaceRole as EUserPermissions
|
currentMemberWorkspaceRole as EUserPermissions
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(ROLE).filter(([key]) => !isGuestOROwner || [currentMemberWorkspaceRole].includes(parseInt(key)))
|
Object.entries(ROLE).filter(([key]) => !isGuestOROwner || [currentMemberWorkspaceRole].includes(parseInt(key)))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { Trash2 } from "lucide-react";
|
import { CircleMinus } from "lucide-react";
|
||||||
import { Disclosure } from "@headlessui/react";
|
import { Disclosure } from "@headlessui/react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { ROLE, EUserPermissions } from "@plane/constants";
|
import { ROLE, EUserPermissions, EUserProjectRoles } from "@plane/constants";
|
||||||
// plane types
|
import { IUser, IWorkspaceMember, TProjectMembership } from "@plane/types";
|
||||||
import { IUser, IWorkspaceMember } from "@plane/types";
|
import { CustomMenu, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// plane ui
|
import { getFileURL } from "@plane/utils";
|
||||||
import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
|
||||||
// constants
|
|
||||||
// helpers
|
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useMember, useUser, useUserPermissions } from "@/hooks/store";
|
import { useMember, useUser, useUserPermissions } from "@/hooks/store";
|
||||||
|
|
||||||
export interface RowData {
|
export interface RowData extends Pick<TProjectMembership, "original_role"> {
|
||||||
member: IWorkspaceMember;
|
member: IWorkspaceMember;
|
||||||
role: EUserPermissions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type NameProps = {
|
type NameProps = {
|
||||||
@@ -44,11 +39,11 @@ export const NameColumn: React.FC<NameProps> = (props) => {
|
|||||||
<Disclosure>
|
<Disclosure>
|
||||||
{({}) => (
|
{({}) => (
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<div className="flex items-center gap-x-4 gap-y-2 w-72 justify-between">
|
<div className="flex items-center gap-2 w-72">
|
||||||
<div className="flex items-center gap-x-2 gap-y-2 flex-1">
|
<div className="flex items-center gap-x-2 gap-y-2 flex-1">
|
||||||
{avatar_url && avatar_url.trim() !== "" ? (
|
{avatar_url && avatar_url.trim() !== "" ? (
|
||||||
<Link href={`/${workspaceSlug}/profile/${id}`}>
|
<Link href={`/${workspaceSlug}/profile/${id}`}>
|
||||||
<span className="relative flex h-4 w-4 items-center justify-center rounded-full capitalize text-white">
|
<span className="relative flex size-4 items-center justify-center rounded-full capitalize text-white">
|
||||||
<img
|
<img
|
||||||
src={getFileURL(avatar_url)}
|
src={getFileURL(avatar_url)}
|
||||||
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
|
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
|
||||||
@@ -58,30 +53,30 @@ export const NameColumn: React.FC<NameProps> = (props) => {
|
|||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<Link href={`/${workspaceSlug}/profile/${id}`}>
|
<Link href={`/${workspaceSlug}/profile/${id}`}>
|
||||||
<span className="relative flex h-4 w-4 text-xs items-center justify-center rounded-full bg-gray-700 capitalize text-white">
|
<span className="relative flex size-4 items-center justify-center rounded-full bg-gray-700 capitalize text-white text-xs">
|
||||||
{(email ?? display_name ?? "?")[0]}
|
{(email ?? display_name ?? "?")[0]}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{first_name} {last_name}
|
{first_name} {last_name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(isAdmin || id === currentUser?.id) && (
|
{(isAdmin || id === currentUser?.id) && (
|
||||||
<PopoverMenu
|
<CustomMenu
|
||||||
data={[""]}
|
ellipsis
|
||||||
keyExtractor={(item) => item}
|
buttonClassName="p-0.5 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
popoverClassName="justify-end"
|
optionsClassName="p-1.5"
|
||||||
buttonClassName="outline-none origin-center rotate-90 size-8 aspect-square flex-shrink-0 grid place-items-center opacity-0 group-hover:opacity-100 transition-opacity"
|
placement="bottom-end"
|
||||||
render={() => (
|
>
|
||||||
|
<CustomMenu.MenuItem>
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-x-3 cursor-pointer"
|
className="flex items-center gap-x-1 cursor-pointer text-red-600 font-medium"
|
||||||
onClick={() => setRemoveMemberModal(rowData)}
|
onClick={() => setRemoveMemberModal(rowData)}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-3.5 align-middle" />
|
<CircleMinus className="flex-shrink-0 size-3.5" />
|
||||||
{rowData.member?.id === currentUser?.id ? "Leave " : "Remove "}
|
{rowData.member?.id === currentUser?.id ? "Leave " : "Remove "}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CustomMenu.MenuItem>
|
||||||
/>
|
</CustomMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,20 +87,20 @@ export const NameColumn: React.FC<NameProps> = (props) => {
|
|||||||
|
|
||||||
export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) => {
|
export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) => {
|
||||||
const { rowData, projectId, workspaceSlug } = props;
|
const { rowData, projectId, workspaceSlug } = props;
|
||||||
|
// store hooks
|
||||||
|
const {
|
||||||
|
project: { updateMemberRole },
|
||||||
|
workspace: { getWorkspaceMemberDetails },
|
||||||
|
} = useMember();
|
||||||
|
const { data: currentUser } = useUser();
|
||||||
|
const { getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||||||
// form info
|
// form info
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm();
|
} = useForm();
|
||||||
// store hooks
|
|
||||||
const {
|
|
||||||
project: { updateMember },
|
|
||||||
workspace: { getWorkspaceMemberDetails },
|
|
||||||
} = useMember();
|
|
||||||
const { data: currentUser } = useUser();
|
|
||||||
const { projectUserInfo } = useUserPermissions();
|
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
|
const roleLabel = ROLE[rowData.original_role ?? EUserPermissions.GUEST];
|
||||||
const isCurrentUser = currentUser?.id === rowData.member.id;
|
const isCurrentUser = currentUser?.id === rowData.member.id;
|
||||||
const isRowDataWorkspaceAdmin = [EUserPermissions.ADMIN].includes(
|
const isRowDataWorkspaceAdmin = [EUserPermissions.ADMIN].includes(
|
||||||
Number(getWorkspaceMemberDetails(rowData.member.id)?.role) ?? EUserPermissions.GUEST
|
Number(getWorkspaceMemberDetails(rowData.member.id)?.role) ?? EUserPermissions.GUEST
|
||||||
@@ -115,8 +110,7 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
|
|||||||
Number(getWorkspaceMemberDetails(currentUser.id)?.role) ?? EUserPermissions.GUEST
|
Number(getWorkspaceMemberDetails(currentUser.id)?.role) ?? EUserPermissions.GUEST
|
||||||
)
|
)
|
||||||
: false;
|
: false;
|
||||||
const currentProjectRole = projectUserInfo?.[workspaceSlug.toString()]?.[projectId.toString()]
|
const currentProjectRole = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
|
||||||
?.role as unknown as EUserPermissions;
|
|
||||||
|
|
||||||
const isCurrentUserProjectAdmin = currentProjectRole
|
const isCurrentUserProjectAdmin = currentProjectRole
|
||||||
? ![EUserPermissions.MEMBER, EUserPermissions.GUEST].includes(Number(currentProjectRole) ?? EUserPermissions.GUEST)
|
? ![EUserPermissions.MEMBER, EUserPermissions.GUEST].includes(Number(currentProjectRole) ?? EUserPermissions.GUEST)
|
||||||
@@ -148,27 +142,26 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
|
|||||||
rules={{ required: "Role is required." }}
|
rules={{ required: "Role is required." }}
|
||||||
render={() => (
|
render={() => (
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={rowData.role?.toString()}
|
value={rowData.original_role}
|
||||||
onChange={(value: EUserPermissions) => {
|
onChange={async (value: EUserProjectRoles) => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
await updateMemberRole(workspaceSlug.toString(), projectId.toString(), rowData.member.id, value).catch(
|
||||||
|
(err) => {
|
||||||
|
console.log(err, "err");
|
||||||
|
const error = err.error;
|
||||||
|
const errorString = Array.isArray(error) ? error[0] : error;
|
||||||
|
|
||||||
updateMember(workspaceSlug.toString(), projectId.toString(), rowData.member.id, {
|
setToast({
|
||||||
role: value as unknown as EUserPermissions, // Cast value to unknown first, then to EUserPermissions
|
type: TOAST_TYPE.ERROR,
|
||||||
}).catch((err) => {
|
title: "You can’t change this role yet.",
|
||||||
console.log(err, "err");
|
message: errorString ?? "An error occurred while updating member role. Please try again.",
|
||||||
const error = err.error;
|
});
|
||||||
const errorString = Array.isArray(error) ? error[0] : error;
|
}
|
||||||
|
);
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "You can’t change this role yet.",
|
|
||||||
message: errorString ?? "An error occurred while updating member role. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
label={
|
label={
|
||||||
<div className="flex ">
|
<div className="flex ">
|
||||||
<span>{ROLE[rowData.role]}</span>
|
<span>{roleLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
|
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
|
||||||
@@ -186,7 +179,7 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-32 flex ">
|
<div className="w-32 flex ">
|
||||||
<span>{ROLE[rowData.role]}</span>
|
<span>{roleLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Loader } from "@plane/ui";
|
|||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { useProject, useUserPermissions, useUserSettings } from "@/hooks/store";
|
import { useProject, useUserPermissions, useUserSettings } from "@/hooks/store";
|
||||||
import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project";
|
import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project";
|
||||||
|
import { getProjectSettingsPageLabelI18nKey } from "@/plane-web/helpers/project-settings";
|
||||||
|
|
||||||
export const NavItemChildren = observer((props: { projectId: string }) => {
|
export const NavItemChildren = observer((props: { projectId: string }) => {
|
||||||
const { projectId } = props;
|
const { projectId } = props;
|
||||||
@@ -65,7 +66,7 @@ export const NavItemChildren = observer((props: { projectId: string }) => {
|
|||||||
"text-sm font-medium"
|
"text-sm font-medium"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{t(link.i18n_label)}
|
{t(getProjectSettingsPageLabelI18nKey(link.key, link.i18n_label))}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import range from "lodash/range";
|
import range from "lodash/range";
|
||||||
|
|
||||||
export const MembersSettingsLoader = () => (
|
export const MembersSettingsLoader = () => (
|
||||||
<div className="divide-y-[0.5px] divide-custom-border-100 animate-pulse">
|
<div className="divide-y-[0.5px] divide-custom-border-100">
|
||||||
{range(4).map((i) => (
|
{range(3).map((i) => (
|
||||||
<div key={i} className="group flex items-center justify-between px-3 py-4">
|
<div key={i} className="group grid grid-cols-5 items-center justify-evenly px-3 py-4">
|
||||||
<div className="flex items-center gap-x-4 gap-y-2">
|
<div className="flex col-span-2 items-center gap-x-2.5">
|
||||||
<span className="h-10 w-10 bg-custom-background-80 rounded-full" />
|
<span className="size-6 bg-custom-background-80 rounded-full" />
|
||||||
<div className="flex flex-col gap-1">
|
<span className="h-5 w-24 bg-custom-background-80 rounded" />
|
||||||
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
|
||||||
<span className="h-4 w-36 bg-custom-background-80 rounded" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="h-6 w-16 bg-custom-background-80 rounded" />
|
<span className="h-5 w-24 bg-custom-background-80 rounded" />
|
||||||
|
<span className="h-5 w-20 bg-custom-background-80 rounded" />
|
||||||
|
<span className="h-5 w-28 bg-custom-background-80 rounded" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ export const WorkspaceMembersList: FC<{ searchQuery: string; isAdmin: boolean }>
|
|||||||
title={
|
title={
|
||||||
<div className="flex w-full items-center justify-between pt-4">
|
<div className="flex w-full items-center justify-between pt-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<h4 className="text-xl font-medium pt-2 pb-2">{t("workspace_settings.settings.members.pending_invites")}</h4>
|
<h4 className="text-xl font-medium pt-2 pb-2">
|
||||||
|
{t("workspace_settings.settings.members.pending_invites")}
|
||||||
|
</h4>
|
||||||
{searchedInvitationsIds && (
|
{searchedInvitationsIds && (
|
||||||
<CountChip count={searchedInvitationsIds.length} className="h-5 m-auto ml-2" />
|
<CountChip count={searchedInvitationsIds.length} className="h-5 m-auto ml-2" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { StoreContext } from "@/lib/store-context";
|
import { StoreContext } from "@/lib/store-context";
|
||||||
// types
|
// plane web imports
|
||||||
import { IUserPermissionStore } from "@/store/user/permissions.store";
|
import { IUserPermissionStore } from "@/plane-web/store/user/permission.store";
|
||||||
|
|
||||||
export const useUserPermissions = (): IUserPermissionStore => {
|
export const useUserPermissions = (): IUserPermissionStore => {
|
||||||
const context = useContext(StoreContext);
|
const context = useContext(StoreContext);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
|||||||
// store hooks
|
// store hooks
|
||||||
const { toggleCreateProjectModal } = useCommandPalette();
|
const { toggleCreateProjectModal } = useCommandPalette();
|
||||||
const { setTrackElement } = useEventTracker();
|
const { setTrackElement } = useEventTracker();
|
||||||
const { fetchUserProjectInfo, allowPermissions, projectUserInfo } = useUserPermissions();
|
const { fetchUserProjectInfo, allowPermissions, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||||||
const { loader, getProjectById, fetchProjectDetails } = useProject();
|
const { loader, getProjectById, fetchProjectDetails } = useProject();
|
||||||
const { fetchAllCycles } = useCycle();
|
const { fetchAllCycles } = useCycle();
|
||||||
const { fetchModulesSlim, fetchModules } = useModule();
|
const { fetchModulesSlim, fetchModules } = useModule();
|
||||||
@@ -64,7 +64,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
|||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const projectExists = projectId ? getProjectById(projectId.toString()) : null;
|
const projectExists = projectId ? getProjectById(projectId.toString()) : null;
|
||||||
const projectMemberInfo = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()];
|
const projectMemberInfo = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
|
||||||
const hasPermissionToCurrentProject = allowPermissions(
|
const hasPermissionToCurrentProject = allowPermissions(
|
||||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
[EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
|
||||||
EUserPermissionsLevel.PROJECT,
|
EUserPermissionsLevel.PROJECT,
|
||||||
@@ -179,7 +179,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
|||||||
projectId &&
|
projectId &&
|
||||||
hasPermissionToCurrentProject === false
|
hasPermissionToCurrentProject === false
|
||||||
)
|
)
|
||||||
return <JoinProject />;
|
return <JoinProject projectId={projectId} />;
|
||||||
|
|
||||||
// check if the project info is not found.
|
// check if the project info is not found.
|
||||||
if (loader === "loaded" && projectId && !!hasPermissionToCurrentProject === false)
|
if (loader === "loaded" && projectId && !!hasPermissionToCurrentProject === false)
|
||||||
|
|||||||
@@ -25,10 +25,13 @@ const PostHogProvider: FC<IPosthogWrapper> = observer((props) => {
|
|||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { instance } = useInstance();
|
const { instance } = useInstance();
|
||||||
const { workspaceSlug, projectId } = useParams();
|
const { workspaceSlug, projectId } = useParams();
|
||||||
const { workspaceInfoBySlug, projectUserInfo } = useUserPermissions();
|
const { getWorkspaceRoleByWorkspaceSlug, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
|
||||||
|
|
||||||
const currentProjectRole = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]?.role;
|
const currentProjectRole = getProjectRoleByWorkspaceSlugAndProjectId(
|
||||||
const currentWorkspaceRole = workspaceInfoBySlug(workspaceSlug?.toString())?.role;
|
workspaceSlug?.toString(),
|
||||||
|
projectId?.toString()
|
||||||
|
);
|
||||||
|
const currentWorkspaceRole = getWorkspaceRoleByWorkspaceSlug(workspaceSlug?.toString());
|
||||||
const is_telemetry_enabled = instance?.is_telemetry_enabled || false;
|
const is_telemetry_enabled = instance?.is_telemetry_enabled || false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ export const sanitizeWorkItemQueries = (
|
|||||||
// Get current project details and user id and role for the project
|
// Get current project details and user id and role for the project
|
||||||
const currentProject = rootStore.projectRoot.project.getProjectById(projectId);
|
const currentProject = rootStore.projectRoot.project.getProjectById(projectId);
|
||||||
const currentUserId = rootStore.user.data?.id;
|
const currentUserId = rootStore.user.data?.id;
|
||||||
const currentUserRole = rootStore.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
|
const currentUserRole = rootStore.user.permission.getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
|
||||||
workspaceSlug,
|
|
||||||
projectId
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only apply this restriction for guests when guest_view_all_features is disabled
|
// Only apply this restriction for guests when guest_view_all_features is disabled
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// types
|
// types
|
||||||
import type { IProjectBulkAddFormData, IProjectMember, IProjectMembership } from "@plane/types";
|
import type { IProjectBulkAddFormData, TProjectMembership } from "@plane/types";
|
||||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||||
// services
|
// services
|
||||||
import { APIService } from "@/services/api.service";
|
import { APIService } from "@/services/api.service";
|
||||||
@@ -9,7 +9,7 @@ export class ProjectMemberService extends APIService {
|
|||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchProjectMembers(workspaceSlug: string, projectId: string): Promise<IProjectMembership[]> {
|
async fetchProjectMembers(workspaceSlug: string, projectId: string): Promise<TProjectMembership[]> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -21,7 +21,7 @@ export class ProjectMemberService extends APIService {
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
data: IProjectBulkAddFormData
|
data: IProjectBulkAddFormData
|
||||||
): Promise<IProjectMembership[]> {
|
): Promise<TProjectMembership[]> {
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/`, data)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -29,7 +29,7 @@ export class ProjectMemberService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async projectMemberMe(workspaceSlug: string, projectId: string): Promise<IProjectMember> {
|
async projectMemberMe(workspaceSlug: string, projectId: string): Promise<TProjectMembership> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -37,7 +37,7 @@ export class ProjectMemberService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMember(workspaceSlug: string, projectId: string, memberId: string): Promise<IProjectMember> {
|
async getProjectMember(workspaceSlug: string, projectId: string, memberId: string): Promise<TProjectMembership> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -49,8 +49,8 @@ export class ProjectMemberService extends APIService {
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
memberId: string,
|
memberId: string,
|
||||||
data: Partial<IProjectMember>
|
data: Partial<TProjectMembership>
|
||||||
): Promise<IProjectMember> {
|
): Promise<TProjectMembership> {
|
||||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`, data)
|
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/members/${memberId}/`, data)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -1,36 +1,35 @@
|
|||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import sortBy from "lodash/sortBy";
|
import sortBy from "lodash/sortBy";
|
||||||
import uniq from "lodash/uniq";
|
import uniq from "lodash/uniq";
|
||||||
|
import unset from "lodash/unset";
|
||||||
import update from "lodash/update";
|
import update from "lodash/update";
|
||||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
// types
|
// plane imports
|
||||||
import { EUserPermissions } from "@plane/constants";
|
import { EUserPermissions, EUserProjectRoles } from "@plane/constants";
|
||||||
import { IProjectBulkAddFormData, IProjectMember, IProjectMembership, IUserLite } from "@plane/types";
|
import { IProjectBulkAddFormData, TProjectMembership, IUserLite } from "@plane/types";
|
||||||
// plane-web constants
|
// plane web imports
|
||||||
|
import { RootStore } from "@/plane-web/store/root.store";
|
||||||
// services
|
// services
|
||||||
import { ProjectMemberService } from "@/services/project";
|
import { ProjectMemberService } from "@/services/project";
|
||||||
// store
|
// store
|
||||||
import { IRouterStore } from "@/store/router.store";
|
import { IRouterStore } from "@/store/router.store";
|
||||||
import { IUserStore } from "@/store/user";
|
import { IUserStore } from "@/store/user";
|
||||||
// store
|
// local imports
|
||||||
import { IProjectStore } from "../project/project.store";
|
import { IProjectStore } from "../project/project.store";
|
||||||
import { CoreRootStore } from "../root.store";
|
|
||||||
import { IMemberRootStore } from ".";
|
import { IMemberRootStore } from ".";
|
||||||
|
|
||||||
export interface IProjectMemberDetails {
|
export interface IProjectMemberDetails extends Omit<TProjectMembership, "member"> {
|
||||||
id: string;
|
|
||||||
member: IUserLite;
|
member: IUserLite;
|
||||||
role: EUserPermissions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProjectMemberStore {
|
export interface IBaseProjectMemberStore {
|
||||||
// observables
|
// observables
|
||||||
projectMemberFetchStatusMap: {
|
projectMemberFetchStatusMap: {
|
||||||
[projectId: string]: boolean;
|
[projectId: string]: boolean;
|
||||||
};
|
};
|
||||||
projectMemberMap: {
|
projectMemberMap: {
|
||||||
[projectId: string]: Record<string, IProjectMembership>;
|
[projectId: string]: Record<string, TProjectMembership>;
|
||||||
};
|
};
|
||||||
// computed
|
// computed
|
||||||
projectMemberIds: string[] | null;
|
projectMemberIds: string[] | null;
|
||||||
@@ -39,41 +38,45 @@ export interface IProjectMemberStore {
|
|||||||
getProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null;
|
getProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null;
|
||||||
getProjectMemberIds: (projectId: string, includeGuestUsers: boolean) => string[] | null;
|
getProjectMemberIds: (projectId: string, includeGuestUsers: boolean) => string[] | null;
|
||||||
// fetch actions
|
// fetch actions
|
||||||
fetchProjectMembers: (workspaceSlug: string, projectId: string) => Promise<IProjectMembership[]>;
|
fetchProjectMembers: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
clearExistingMembers?: boolean
|
||||||
|
) => Promise<TProjectMembership[]>;
|
||||||
// bulk operation actions
|
// bulk operation actions
|
||||||
bulkAddMembersToProject: (
|
bulkAddMembersToProject: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
data: IProjectBulkAddFormData
|
data: IProjectBulkAddFormData
|
||||||
) => Promise<IProjectMembership[]>;
|
) => Promise<TProjectMembership[]>;
|
||||||
// crud actions
|
// crud actions
|
||||||
updateMember: (
|
updateMemberRole: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
data: { role: EUserPermissions }
|
role: EUserProjectRoles
|
||||||
) => Promise<IProjectMember>;
|
) => Promise<TProjectMembership>;
|
||||||
removeMemberFromProject: (workspaceSlug: string, projectId: string, userId: string) => Promise<void>;
|
removeMemberFromProject: (workspaceSlug: string, projectId: string, userId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProjectMemberStore implements IProjectMemberStore {
|
export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore {
|
||||||
// observables
|
// observables
|
||||||
projectMemberFetchStatusMap: {
|
projectMemberFetchStatusMap: {
|
||||||
[projectId: string]: boolean;
|
[projectId: string]: boolean;
|
||||||
} = {};
|
} = {};
|
||||||
projectMemberMap: {
|
projectMemberMap: {
|
||||||
[projectId: string]: Record<string, IProjectMembership>;
|
[projectId: string]: Record<string, TProjectMembership>;
|
||||||
} = {};
|
} = {};
|
||||||
// stores
|
// stores
|
||||||
routerStore: IRouterStore;
|
routerStore: IRouterStore;
|
||||||
userStore: IUserStore;
|
userStore: IUserStore;
|
||||||
memberRoot: IMemberRootStore;
|
memberRoot: IMemberRootStore;
|
||||||
projectRoot: IProjectStore;
|
projectRoot: IProjectStore;
|
||||||
rootStore: CoreRootStore;
|
rootStore: RootStore;
|
||||||
// services
|
// services
|
||||||
projectMemberService;
|
projectMemberService;
|
||||||
|
|
||||||
constructor(_memberRoot: IMemberRootStore, _rootStore: CoreRootStore) {
|
constructor(_memberRoot: IMemberRootStore, _rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// observables
|
// observables
|
||||||
projectMemberMap: observable,
|
projectMemberMap: observable,
|
||||||
@@ -82,7 +85,7 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
|||||||
// actions
|
// actions
|
||||||
fetchProjectMembers: action,
|
fetchProjectMembers: action,
|
||||||
bulkAddMembersToProject: action,
|
bulkAddMembersToProject: action,
|
||||||
updateMember: action,
|
updateMemberRole: action,
|
||||||
removeMemberFromProject: action,
|
removeMemberFromProject: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,6 +106,7 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
|||||||
const projectId = this.routerStore.projectId;
|
const projectId = this.routerStore.projectId;
|
||||||
if (!projectId) return null;
|
if (!projectId) return null;
|
||||||
let members = Object.values(this.projectMemberMap?.[projectId] ?? {});
|
let members = Object.values(this.projectMemberMap?.[projectId] ?? {});
|
||||||
|
if (members.length === 0) return null;
|
||||||
members = sortBy(members, [
|
members = sortBy(members, [
|
||||||
(m) => m.member !== this.userStore.data?.id,
|
(m) => m.member !== this.userStore.data?.id,
|
||||||
(m) => this.memberRoot.memberMap?.[m.member]?.display_name.toLowerCase(),
|
(m) => this.memberRoot.memberMap?.[m.member]?.display_name.toLowerCase(),
|
||||||
@@ -117,20 +121,64 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
|||||||
*/
|
*/
|
||||||
getProjectMemberFetchStatus = computedFn((projectId: string) => this.projectMemberFetchStatusMap?.[projectId]);
|
getProjectMemberFetchStatus = computedFn((projectId: string) => this.projectMemberFetchStatusMap?.[projectId]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get the project memberships
|
||||||
|
* @param projectId
|
||||||
|
*/
|
||||||
|
protected getProjectMemberships = computedFn((projectId: string) =>
|
||||||
|
Object.values(this.projectMemberMap?.[projectId] ?? {})
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get the project membership by user id
|
||||||
|
* @param userId
|
||||||
|
* @param projectId
|
||||||
|
*/
|
||||||
|
protected getProjectMembershipByUserId = computedFn(
|
||||||
|
(userId: string, projectId: string) => this.projectMemberMap?.[projectId]?.[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get the role from the project membership
|
||||||
|
* @param userId
|
||||||
|
* @param projectId
|
||||||
|
*/
|
||||||
|
protected getRoleFromProjectMembership = computedFn(
|
||||||
|
(userId: string, projectId: string): EUserProjectRoles | undefined => {
|
||||||
|
const projectMembership = this.getProjectMembershipByUserId(userId, projectId);
|
||||||
|
if (!projectMembership) return undefined;
|
||||||
|
const projectMembershipRole = projectMembership.original_role ?? projectMembership.role;
|
||||||
|
return projectMembershipRole ? (projectMembershipRole as EUserProjectRoles) : undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns the project membership role for a user
|
||||||
|
* @description This method is specifically used when adding new members to a project. For existing members,
|
||||||
|
* the role is fetched directly from the backend during member listing.
|
||||||
|
* @param { string } userId - The ID of the user
|
||||||
|
* @param { string } projectId - The ID of the project
|
||||||
|
* @returns { EUserProjectRoles | undefined } The user's role in the project, or undefined if not found
|
||||||
|
*/
|
||||||
|
abstract getUserProjectRole: (userId: string, projectId: string) => EUserProjectRoles | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get the details of a project member
|
* @description get the details of a project member
|
||||||
* @param userId
|
* @param userId
|
||||||
|
* @param projectId
|
||||||
*/
|
*/
|
||||||
getProjectMemberDetails = computedFn((userId: string, projectId: string) => {
|
getProjectMemberDetails = computedFn((userId: string, projectId: string) => {
|
||||||
const projectMember = this.projectMemberMap?.[projectId]?.[userId];
|
const projectMember = this.getProjectMembershipByUserId(userId, projectId);
|
||||||
if (!projectMember) return null;
|
if (!projectMember) return null;
|
||||||
const memberDetails: IProjectMemberDetails = {
|
const memberDetails: IProjectMemberDetails = {
|
||||||
id: projectMember.id,
|
id: projectMember.id,
|
||||||
role: projectMember.role,
|
role: projectMember.role,
|
||||||
|
original_role: projectMember.original_role,
|
||||||
member: {
|
member: {
|
||||||
...this.memberRoot?.memberMap?.[projectMember.member],
|
...this.memberRoot?.memberMap?.[projectMember.member],
|
||||||
joining_date: projectMember.created_at,
|
joining_date: projectMember.created_at,
|
||||||
},
|
},
|
||||||
|
created_at: projectMember.created_at,
|
||||||
};
|
};
|
||||||
return memberDetails;
|
return memberDetails;
|
||||||
});
|
});
|
||||||
@@ -141,7 +189,7 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
|||||||
*/
|
*/
|
||||||
getProjectMemberIds = computedFn((projectId: string, includeGuestUsers: boolean): string[] | null => {
|
getProjectMemberIds = computedFn((projectId: string, includeGuestUsers: boolean): string[] | null => {
|
||||||
if (!this.projectMemberMap?.[projectId]) return null;
|
if (!this.projectMemberMap?.[projectId]) return null;
|
||||||
let members = Object.values(this.projectMemberMap?.[projectId]);
|
let members = this.getProjectMemberships(projectId);
|
||||||
if (includeGuestUsers === false) {
|
if (includeGuestUsers === false) {
|
||||||
members = members.filter((m) => m.role !== EUserPermissions.GUEST);
|
members = members.filter((m) => m.role !== EUserPermissions.GUEST);
|
||||||
}
|
}
|
||||||
@@ -158,9 +206,12 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
|||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
* @param projectId
|
* @param projectId
|
||||||
*/
|
*/
|
||||||
fetchProjectMembers = async (workspaceSlug: string, projectId: string) =>
|
fetchProjectMembers = async (workspaceSlug: string, projectId: string, clearExistingMembers: boolean = false) =>
|
||||||
await this.projectMemberService.fetchProjectMembers(workspaceSlug, projectId).then((response) => {
|
await this.projectMemberService.fetchProjectMembers(workspaceSlug, projectId).then((response) => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
if (clearExistingMembers) {
|
||||||
|
unset(this.projectMemberMap, [projectId]);
|
||||||
|
}
|
||||||
response.forEach((member) => {
|
response.forEach((member) => {
|
||||||
set(this.projectMemberMap, [projectId, member.member], member);
|
set(this.projectMemberMap, [projectId, member.member], member);
|
||||||
});
|
});
|
||||||
@@ -174,13 +225,17 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
|||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
* @param projectId
|
* @param projectId
|
||||||
* @param data
|
* @param data
|
||||||
* @returns Promise<IProjectMembership[]>
|
* @returns Promise<TProjectMembership[]>
|
||||||
*/
|
*/
|
||||||
bulkAddMembersToProject = async (workspaceSlug: string, projectId: string, data: IProjectBulkAddFormData) =>
|
bulkAddMembersToProject = async (workspaceSlug: string, projectId: string, data: IProjectBulkAddFormData) =>
|
||||||
await this.projectMemberService.bulkAddMembersToProject(workspaceSlug, projectId, data).then((response) => {
|
await this.projectMemberService.bulkAddMembersToProject(workspaceSlug, projectId, data).then((response) => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
response.forEach((member) => {
|
response.forEach((member) => {
|
||||||
set(this.projectMemberMap, [projectId, member.member], member);
|
set(this.projectMemberMap, [projectId, member.member], {
|
||||||
|
...member,
|
||||||
|
role: this.getUserProjectRole(member.member, projectId) ?? member.role,
|
||||||
|
original_role: member.role,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
update(this.projectRoot.projectMap, [projectId, "members"], (memberIds) =>
|
update(this.projectRoot.projectMap, [projectId, "members"], (memberIds) =>
|
||||||
@@ -193,6 +248,18 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
|||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description update the role of a member in a project
|
||||||
|
* @param projectId
|
||||||
|
* @param userId
|
||||||
|
* @param role
|
||||||
|
*/
|
||||||
|
abstract getProjectMemberRoleForUpdate: (
|
||||||
|
projectId: string,
|
||||||
|
userId: string,
|
||||||
|
role: EUserProjectRoles
|
||||||
|
) => EUserProjectRoles;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description update the role of a member in a project
|
* @description update the role of a member in a project
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
@@ -200,40 +267,82 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
|||||||
* @param userId
|
* @param userId
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
updateMember = async (workspaceSlug: string, projectId: string, userId: string, data: { role: EUserPermissions }) => {
|
updateMemberRole = async (workspaceSlug: string, projectId: string, userId: string, role: EUserProjectRoles) => {
|
||||||
const memberDetails = this.getProjectMemberDetails(userId, projectId);
|
const memberDetails = this.getProjectMemberDetails(userId, projectId);
|
||||||
if (!memberDetails) throw new Error("Member not found");
|
if (!memberDetails || !memberDetails?.id) throw new Error("Member not found");
|
||||||
// original data to revert back in case of error
|
// original data to revert back in case of error
|
||||||
const originalProjectMemberData = this.projectMemberMap?.[projectId]?.[userId]?.role;
|
|
||||||
const isCurrentUser = this.rootStore.user.data?.id === userId;
|
const isCurrentUser = this.rootStore.user.data?.id === userId;
|
||||||
|
const membershipBeforeUpdate = this.getProjectMembershipByUserId(userId, projectId);
|
||||||
|
const permissionBeforeUpdate = isCurrentUser
|
||||||
|
? this.rootStore.user.permission.getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId)
|
||||||
|
: undefined;
|
||||||
|
const updatedProjectRole = this.getProjectMemberRoleForUpdate(projectId, userId, role);
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.projectMemberMap, [projectId, userId, "role"], data.role);
|
set(this.projectMemberMap, [projectId, userId, "original_role"], role);
|
||||||
if (isCurrentUser)
|
set(this.projectMemberMap, [projectId, userId, "role"], updatedProjectRole);
|
||||||
set(this.rootStore.user.permission.projectUserInfo, [workspaceSlug, projectId, "role"], data.role);
|
if (isCurrentUser) {
|
||||||
|
set(
|
||||||
|
this.rootStore.user.permission.workspaceProjectsPermissions,
|
||||||
|
[workspaceSlug, projectId],
|
||||||
|
updatedProjectRole
|
||||||
|
);
|
||||||
|
}
|
||||||
|
set(this.rootStore.user.permission.projectUserInfo, [workspaceSlug, projectId, "role"], updatedProjectRole);
|
||||||
});
|
});
|
||||||
const response = await this.projectMemberService.updateProjectMember(
|
const response = await this.projectMemberService.updateProjectMember(
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
projectId,
|
projectId,
|
||||||
memberDetails?.id,
|
memberDetails?.id,
|
||||||
data
|
{
|
||||||
|
role,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// revert back to original members in case of error
|
// revert back to original members in case of error
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.projectMemberMap, [projectId, userId, "role"], originalProjectMemberData);
|
set(this.projectMemberMap, [projectId, userId, "original_role"], membershipBeforeUpdate?.original_role);
|
||||||
if (isCurrentUser)
|
set(this.projectMemberMap, [projectId, userId, "role"], membershipBeforeUpdate?.role);
|
||||||
|
if (isCurrentUser) {
|
||||||
|
set(
|
||||||
|
this.rootStore.user.permission.workspaceProjectsPermissions,
|
||||||
|
[workspaceSlug, projectId],
|
||||||
|
membershipBeforeUpdate?.original_role
|
||||||
|
);
|
||||||
set(
|
set(
|
||||||
this.rootStore.user.permission.projectUserInfo,
|
this.rootStore.user.permission.projectUserInfo,
|
||||||
[workspaceSlug, projectId, "role"],
|
[workspaceSlug, projectId, "role"],
|
||||||
originalProjectMemberData
|
permissionBeforeUpdate
|
||||||
);
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Handles the removal of a member from a project
|
||||||
|
* @param projectId - The ID of the project to remove the member from
|
||||||
|
* @param userId - The ID of the user to remove from the project
|
||||||
|
*/
|
||||||
|
protected handleMemberRemoval = (projectId: string, userId: string) => {
|
||||||
|
unset(this.projectMemberMap, [projectId, userId]);
|
||||||
|
set(
|
||||||
|
this.projectRoot.projectMap,
|
||||||
|
[projectId, "members"],
|
||||||
|
this.projectRoot.projectMap?.[projectId]?.members?.filter((memberId) => memberId !== userId)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Processes the removal of a member from a project
|
||||||
|
* This abstract method handles the cleanup of member data from the project member map
|
||||||
|
* @param projectId - The ID of the project to remove the member from
|
||||||
|
* @param userId - The ID of the user to remove from the project
|
||||||
|
*/
|
||||||
|
abstract processMemberRemoval: (projectId: string, userId: string) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description remove a member from a project
|
* @description remove a member from a project
|
||||||
* @param workspaceSlug
|
* @param workspaceSlug
|
||||||
@@ -242,14 +351,11 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
|||||||
*/
|
*/
|
||||||
removeMemberFromProject = async (workspaceSlug: string, projectId: string, userId: string) => {
|
removeMemberFromProject = async (workspaceSlug: string, projectId: string, userId: string) => {
|
||||||
const memberDetails = this.getProjectMemberDetails(userId, projectId);
|
const memberDetails = this.getProjectMemberDetails(userId, projectId);
|
||||||
if (!memberDetails) throw new Error("Member not found");
|
if (!memberDetails || !memberDetails?.id) throw new Error("Member not found");
|
||||||
await this.projectMemberService.deleteProjectMember(workspaceSlug, projectId, memberDetails?.id).then(() => {
|
await this.projectMemberService.deleteProjectMember(workspaceSlug, projectId, memberDetails?.id).then(() => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
delete this.projectMemberMap?.[projectId]?.[userId];
|
this.processMemberRemoval(projectId, userId);
|
||||||
});
|
});
|
||||||
this.projectRoot.projectMap[projectId].members = this.projectRoot.projectMap?.[projectId]?.members?.filter(
|
|
||||||
(memberId) => memberId !== userId
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { makeObservable, observable } from "mobx";
|
import { makeObservable, observable } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
// type
|
// plane imports
|
||||||
import { IUserLite } from "@plane/types";
|
import { IUserLite } from "@plane/types";
|
||||||
// store
|
// plane web imports
|
||||||
import { CoreRootStore } from "../root.store";
|
import { IProjectMemberStore, ProjectMemberStore } from "@/plane-web/store/member/project-member.store";
|
||||||
import { IProjectMemberStore, ProjectMemberStore } from "./project-member.store";
|
import { RootStore } from "@/plane-web/store/root.store";
|
||||||
|
// local imports
|
||||||
import { IWorkspaceMemberStore, WorkspaceMemberStore } from "./workspace-member.store";
|
import { IWorkspaceMemberStore, WorkspaceMemberStore } from "./workspace-member.store";
|
||||||
|
|
||||||
export interface IMemberRootStore {
|
export interface IMemberRootStore {
|
||||||
@@ -25,7 +26,7 @@ export class MemberRootStore implements IMemberRootStore {
|
|||||||
workspace: IWorkspaceMemberStore;
|
workspace: IWorkspaceMemberStore;
|
||||||
project: IProjectMemberStore;
|
project: IProjectMemberStore;
|
||||||
|
|
||||||
constructor(_rootStore: CoreRootStore) {
|
constructor(_rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// observables
|
// observables
|
||||||
memberMap: observable,
|
memberMap: observable,
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
|||||||
*/
|
*/
|
||||||
get canCurrentUserCreatePage() {
|
get canCurrentUserCreatePage() {
|
||||||
const { workspaceSlug, projectId } = this.store.router;
|
const { workspaceSlug, projectId } = this.store.router;
|
||||||
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
|
const currentUserProjectRole = this.store.user.permission.getProjectRoleByWorkspaceSlugAndProjectId(
|
||||||
workspaceSlug?.toString() || "",
|
workspaceSlug?.toString() || "",
|
||||||
projectId?.toString() || ""
|
projectId?.toString() || ""
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export class ProjectPage extends BasePage implements TProjectPage {
|
|||||||
if (!workspaceSlug || !this.project_ids?.length) return;
|
if (!workspaceSlug || !this.project_ids?.length) return;
|
||||||
let highestRole: EUserPermissions | undefined = undefined;
|
let highestRole: EUserPermissions | undefined = undefined;
|
||||||
this.project_ids.map((projectId) => {
|
this.project_ids.map((projectId) => {
|
||||||
const currentUserProjectRole = this.rootStore.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
|
const currentUserProjectRole = this.rootStore.user.permission.getProjectRoleByWorkspaceSlugAndProjectId(
|
||||||
workspaceSlug?.toString() || "",
|
workspaceSlug?.toString() || "",
|
||||||
projectId?.toString() || ""
|
projectId?.toString() || ""
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ export class CoreRootStore {
|
|||||||
this.router = new RouterStore();
|
this.router = new RouterStore();
|
||||||
this.commandPalette = new CommandPaletteStore();
|
this.commandPalette = new CommandPaletteStore();
|
||||||
this.instance = new InstanceStore();
|
this.instance = new InstanceStore();
|
||||||
this.user = new UserStore(this);
|
this.user = new UserStore(this as unknown as RootStore);
|
||||||
this.theme = new ThemeStore();
|
this.theme = new ThemeStore();
|
||||||
this.workspaceRoot = new WorkspaceRootStore(this);
|
this.workspaceRoot = new WorkspaceRootStore(this);
|
||||||
this.projectRoot = new ProjectRootStore(this);
|
this.projectRoot = new ProjectRootStore(this);
|
||||||
this.memberRoot = new MemberRootStore(this);
|
this.memberRoot = new MemberRootStore(this as unknown as RootStore);
|
||||||
this.cycle = new CycleStore(this);
|
this.cycle = new CycleStore(this);
|
||||||
this.cycleFilter = new CycleFilterStore(this);
|
this.cycleFilter = new CycleFilterStore(this);
|
||||||
this.module = new ModulesStore(this);
|
this.module = new ModulesStore(this);
|
||||||
@@ -106,10 +106,10 @@ export class CoreRootStore {
|
|||||||
this.router = new RouterStore();
|
this.router = new RouterStore();
|
||||||
this.commandPalette = new CommandPaletteStore();
|
this.commandPalette = new CommandPaletteStore();
|
||||||
this.instance = new InstanceStore();
|
this.instance = new InstanceStore();
|
||||||
this.user = new UserStore(this);
|
this.user = new UserStore(this as unknown as RootStore);
|
||||||
this.workspaceRoot = new WorkspaceRootStore(this);
|
this.workspaceRoot = new WorkspaceRootStore(this);
|
||||||
this.projectRoot = new ProjectRootStore(this);
|
this.projectRoot = new ProjectRootStore(this);
|
||||||
this.memberRoot = new MemberRootStore(this);
|
this.memberRoot = new MemberRootStore(this as unknown as RootStore);
|
||||||
this.cycle = new CycleStore(this);
|
this.cycle = new CycleStore(this);
|
||||||
this.cycleFilter = new CycleFilterStore(this);
|
this.cycleFilter = new CycleFilterStore(this);
|
||||||
this.module = new ModulesStore(this);
|
this.module = new ModulesStore(this);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import set from "lodash/set";
|
|||||||
import unset from "lodash/unset";
|
import unset from "lodash/unset";
|
||||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||||
import { computedFn } from "mobx-utils";
|
import { computedFn } from "mobx-utils";
|
||||||
// types
|
// plane imports
|
||||||
import {
|
import {
|
||||||
EUserProjectRoles,
|
EUserProjectRoles,
|
||||||
EUserWorkspaceRoles,
|
EUserWorkspaceRoles,
|
||||||
@@ -10,35 +10,32 @@ import {
|
|||||||
EUserPermissionsLevel,
|
EUserPermissionsLevel,
|
||||||
TUserPermissions,
|
TUserPermissions,
|
||||||
TUserPermissionsLevel,
|
TUserPermissionsLevel,
|
||||||
|
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS,
|
||||||
} from "@plane/constants";
|
} from "@plane/constants";
|
||||||
import { IProjectMember, IUserProjectsRole, IWorkspaceMemberMe } from "@plane/types";
|
import { TProjectMembership, IUserProjectsRole, IWorkspaceMemberMe } from "@plane/types";
|
||||||
// plane web types
|
// plane web imports
|
||||||
// plane web services
|
|
||||||
import { WorkspaceService } from "@/plane-web/services/workspace.service";
|
import { WorkspaceService } from "@/plane-web/services/workspace.service";
|
||||||
|
import { RootStore } from "@/plane-web/store/root.store";
|
||||||
// services
|
// services
|
||||||
import projectMemberService from "@/services/project/project-member.service";
|
import projectMemberService from "@/services/project/project-member.service";
|
||||||
import userService from "@/services/user.service";
|
import userService from "@/services/user.service";
|
||||||
// store
|
|
||||||
import { CoreRootStore } from "@/store/root.store";
|
|
||||||
|
|
||||||
// derived services
|
// derived services
|
||||||
const workspaceService = new WorkspaceService();
|
const workspaceService = new WorkspaceService();
|
||||||
|
|
||||||
type ETempUserRole = TUserPermissions | EUserWorkspaceRoles | EUserProjectRoles; // TODO: Remove this once we have migrated user permissions to enums to plane constants package
|
type ETempUserRole = TUserPermissions | EUserWorkspaceRoles | EUserProjectRoles; // TODO: Remove this once we have migrated user permissions to enums to plane constants package
|
||||||
|
|
||||||
export interface IUserPermissionStore {
|
export interface IBaseUserPermissionStore {
|
||||||
loader: boolean;
|
loader: boolean;
|
||||||
// observables
|
// observables
|
||||||
workspaceUserInfo: Record<string, IWorkspaceMemberMe>; // workspaceSlug -> IWorkspaceMemberMe
|
workspaceUserInfo: Record<string, IWorkspaceMemberMe>; // workspaceSlug -> IWorkspaceMemberMe
|
||||||
projectUserInfo: Record<string, Record<string, IProjectMember>>; // workspaceSlug -> projectId -> IProjectMember
|
projectUserInfo: Record<string, Record<string, TProjectMembership>>; // workspaceSlug -> projectId -> TProjectMembership
|
||||||
workspaceProjectsPermissions: Record<string, IUserProjectsRole>; // workspaceSlug -> IUserProjectsRole
|
workspaceProjectsPermissions: Record<string, IUserProjectsRole>; // workspaceSlug -> IUserProjectsRole
|
||||||
// computed
|
|
||||||
// computed helpers
|
// computed helpers
|
||||||
workspaceInfoBySlug: (workspaceSlug: string) => IWorkspaceMemberMe | undefined;
|
workspaceInfoBySlug: (workspaceSlug: string) => IWorkspaceMemberMe | undefined;
|
||||||
projectPermissionsByWorkspaceSlugAndProjectId: (
|
getWorkspaceRoleByWorkspaceSlug: (workspaceSlug: string) => TUserPermissions | EUserWorkspaceRoles | undefined;
|
||||||
workspaceSlug: string,
|
getProjectRolesByWorkspaceSlug: (workspaceSlug: string) => IUserProjectsRole;
|
||||||
projectId: string
|
getProjectRoleByWorkspaceSlugAndProjectId: (workspaceSlug: string, projectId: string) => EUserPermissions | undefined;
|
||||||
) => TUserPermissions | undefined;
|
|
||||||
allowPermissions: (
|
allowPermissions: (
|
||||||
allowPermissions: ETempUserRole[],
|
allowPermissions: ETempUserRole[],
|
||||||
level: TUserPermissionsLevel,
|
level: TUserPermissionsLevel,
|
||||||
@@ -46,25 +43,29 @@ export interface IUserPermissionStore {
|
|||||||
projectId?: string,
|
projectId?: string,
|
||||||
onPermissionAllowed?: () => boolean
|
onPermissionAllowed?: () => boolean
|
||||||
) => boolean;
|
) => boolean;
|
||||||
// action helpers
|
|
||||||
// actions
|
// actions
|
||||||
fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise<IWorkspaceMemberMe | undefined>;
|
fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise<IWorkspaceMemberMe>;
|
||||||
leaveWorkspace: (workspaceSlug: string) => Promise<void>;
|
leaveWorkspace: (workspaceSlug: string) => Promise<void>;
|
||||||
fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise<IProjectMember | undefined>;
|
fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise<TProjectMembership>;
|
||||||
fetchUserProjectPermissions: (workspaceSlug: string) => Promise<IUserProjectsRole | undefined>;
|
fetchUserProjectPermissions: (workspaceSlug: string) => Promise<IUserProjectsRole>;
|
||||||
joinProject: (workspaceSlug: string, projectId: string) => Promise<void | undefined>;
|
joinProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||||
leaveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
leaveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||||
|
hasPageAccess: (workspaceSlug: string, key: string) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserPermissionStore implements IUserPermissionStore {
|
/**
|
||||||
|
* @description This store is used to handle permission layer for the currently logged user.
|
||||||
|
* It manages workspace and project level permissions, roles and access control.
|
||||||
|
*/
|
||||||
|
export abstract class BaseUserPermissionStore implements IBaseUserPermissionStore {
|
||||||
loader: boolean = false;
|
loader: boolean = false;
|
||||||
// constants
|
// constants
|
||||||
workspaceUserInfo: Record<string, IWorkspaceMemberMe> = {};
|
workspaceUserInfo: Record<string, IWorkspaceMemberMe> = {};
|
||||||
projectUserInfo: Record<string, Record<string, IProjectMember>> = {};
|
projectUserInfo: Record<string, Record<string, TProjectMembership>> = {};
|
||||||
workspaceProjectsPermissions: Record<string, IUserProjectsRole> = {};
|
workspaceProjectsPermissions: Record<string, IUserProjectsRole> = {};
|
||||||
// observables
|
// observables
|
||||||
|
|
||||||
constructor(private store: CoreRootStore) {
|
constructor(protected store: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// observables
|
// observables
|
||||||
loader: observable.ref,
|
loader: observable.ref,
|
||||||
@@ -82,8 +83,6 @@ export class UserPermissionStore implements IUserPermissionStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// computed
|
|
||||||
|
|
||||||
// computed helpers
|
// computed helpers
|
||||||
/**
|
/**
|
||||||
* @description Returns the current workspace information
|
* @description Returns the current workspace information
|
||||||
@@ -95,18 +94,69 @@ export class UserPermissionStore implements IUserPermissionStore {
|
|||||||
return this.workspaceUserInfo[workspaceSlug] || undefined;
|
return this.workspaceUserInfo[workspaceSlug] || undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns the workspace role by slug
|
||||||
|
* @param { string } workspaceSlug
|
||||||
|
* @returns { TUserPermissions | EUserWorkspaceRoles | undefined }
|
||||||
|
*/
|
||||||
|
getWorkspaceRoleByWorkspaceSlug = computedFn(
|
||||||
|
(workspaceSlug: string): TUserPermissions | EUserWorkspaceRoles | undefined => {
|
||||||
|
if (!workspaceSlug) return undefined;
|
||||||
|
return this.workspaceUserInfo[workspaceSlug]?.role as TUserPermissions | EUserWorkspaceRoles | undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns the project membership permission
|
||||||
|
* @param { string } workspaceSlug
|
||||||
|
* @param { string } projectId
|
||||||
|
* @returns { EUserPermissions | undefined }
|
||||||
|
*/
|
||||||
|
protected getProjectRole = computedFn((workspaceSlug: string, projectId: string): EUserPermissions | undefined => {
|
||||||
|
if (!workspaceSlug || !projectId) return undefined;
|
||||||
|
return this.workspaceProjectsPermissions?.[workspaceSlug]?.[projectId] || undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns the project permissions by workspace slug
|
||||||
|
* @param { string } workspaceSlug
|
||||||
|
* @returns { IUserProjectsRole }
|
||||||
|
*/
|
||||||
|
getProjectRolesByWorkspaceSlug = computedFn((workspaceSlug: string): IUserProjectsRole => {
|
||||||
|
const projectPermissions = this.workspaceProjectsPermissions[workspaceSlug] || {};
|
||||||
|
return Object.keys(projectPermissions).reduce((acc, projectId) => {
|
||||||
|
const projectRole = this.getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
|
||||||
|
if (projectRole) {
|
||||||
|
acc[projectId] = projectRole;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as IUserProjectsRole);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Returns the current project permissions
|
* @description Returns the current project permissions
|
||||||
* @param { string } workspaceSlug
|
* @param { string } workspaceSlug
|
||||||
* @param { string } projectId
|
* @param { string } projectId
|
||||||
* @returns { IUserProjectsRole | undefined }
|
* @returns { EUserPermissions | undefined }
|
||||||
*/
|
*/
|
||||||
projectPermissionsByWorkspaceSlugAndProjectId = computedFn(
|
abstract getProjectRoleByWorkspaceSlugAndProjectId: (
|
||||||
(workspaceSlug: string, projectId: string): TUserPermissions | undefined => {
|
workspaceSlug: string,
|
||||||
if (!workspaceSlug || !projectId) return undefined;
|
projectId: string
|
||||||
return this.workspaceProjectsPermissions?.[workspaceSlug]?.[projectId] || undefined;
|
) => EUserPermissions | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns whether the user has the permission to access a page
|
||||||
|
* @param { string } page
|
||||||
|
* @returns { boolean }
|
||||||
|
*/
|
||||||
|
hasPageAccess = computedFn((workspaceSlug: string, key: string): boolean => {
|
||||||
|
if (!workspaceSlug || !key) return false;
|
||||||
|
const settings = WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.find((item) => item.key === key);
|
||||||
|
if (settings) {
|
||||||
|
return this.allowPermissions(settings.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug);
|
||||||
}
|
}
|
||||||
);
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
// action helpers
|
// action helpers
|
||||||
/**
|
/**
|
||||||
@@ -132,19 +182,22 @@ export class UserPermissionStore implements IUserPermissionStore {
|
|||||||
let currentUserRole: TUserPermissions | undefined = undefined;
|
let currentUserRole: TUserPermissions | undefined = undefined;
|
||||||
|
|
||||||
if (level === EUserPermissionsLevel.WORKSPACE) {
|
if (level === EUserPermissionsLevel.WORKSPACE) {
|
||||||
const workspaceInfoBySlug = workspaceSlug && this.workspaceInfoBySlug(workspaceSlug);
|
currentUserRole = (workspaceSlug && this.getWorkspaceRoleByWorkspaceSlug(workspaceSlug)) as
|
||||||
if (workspaceInfoBySlug) {
|
| EUserPermissions
|
||||||
currentUserRole = workspaceInfoBySlug?.role as unknown as EUserPermissions;
|
| undefined;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (level === EUserPermissionsLevel.PROJECT) {
|
if (level === EUserPermissionsLevel.PROJECT) {
|
||||||
currentUserRole = (workspaceSlug &&
|
currentUserRole = (workspaceSlug &&
|
||||||
projectId &&
|
projectId &&
|
||||||
this.projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId)) as EUserPermissions | undefined;
|
this.getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId)) as EUserPermissions | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentUserRole && allowPermissions.includes(currentUserRole as TUserPermissions)) {
|
if (typeof currentUserRole === "string") {
|
||||||
|
currentUserRole = parseInt(currentUserRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUserRole && typeof currentUserRole === "number" && allowPermissions.includes(currentUserRole)) {
|
||||||
if (onPermissionAllowed) {
|
if (onPermissionAllowed) {
|
||||||
return onPermissionAllowed();
|
return onPermissionAllowed();
|
||||||
} else {
|
} else {
|
||||||
@@ -159,9 +212,9 @@ export class UserPermissionStore implements IUserPermissionStore {
|
|||||||
/**
|
/**
|
||||||
* @description Fetches the user's workspace information
|
* @description Fetches the user's workspace information
|
||||||
* @param { string } workspaceSlug
|
* @param { string } workspaceSlug
|
||||||
* @returns { Promise<void | undefined> }
|
* @returns { Promise<IWorkspaceMemberMe | undefined> }
|
||||||
*/
|
*/
|
||||||
fetchUserWorkspaceInfo = async (workspaceSlug: string): Promise<IWorkspaceMemberMe | undefined> => {
|
fetchUserWorkspaceInfo = async (workspaceSlug: string): Promise<IWorkspaceMemberMe> => {
|
||||||
try {
|
try {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
const response = await workspaceService.workspaceMemberMe(workspaceSlug);
|
const response = await workspaceService.workspaceMemberMe(workspaceSlug);
|
||||||
@@ -202,9 +255,9 @@ export class UserPermissionStore implements IUserPermissionStore {
|
|||||||
* @description Fetches the user's project information
|
* @description Fetches the user's project information
|
||||||
* @param { string } workspaceSlug
|
* @param { string } workspaceSlug
|
||||||
* @param { string } projectId
|
* @param { string } projectId
|
||||||
* @returns { Promise<void | undefined> }
|
* @returns { Promise<TProjectMembership | undefined> }
|
||||||
*/
|
*/
|
||||||
fetchUserProjectInfo = async (workspaceSlug: string, projectId: string): Promise<IProjectMember | undefined> => {
|
fetchUserProjectInfo = async (workspaceSlug: string, projectId: string): Promise<TProjectMembership> => {
|
||||||
try {
|
try {
|
||||||
const response = await projectMemberService.projectMemberMe(workspaceSlug, projectId);
|
const response = await projectMemberService.projectMemberMe(workspaceSlug, projectId);
|
||||||
if (response) {
|
if (response) {
|
||||||
@@ -223,9 +276,9 @@ export class UserPermissionStore implements IUserPermissionStore {
|
|||||||
/**
|
/**
|
||||||
* @description Fetches the user's project permissions
|
* @description Fetches the user's project permissions
|
||||||
* @param { string } workspaceSlug
|
* @param { string } workspaceSlug
|
||||||
* @returns { Promise<void | undefined> }
|
* @returns { Promise<IUserProjectsRole | undefined> }
|
||||||
*/
|
*/
|
||||||
fetchUserProjectPermissions = async (workspaceSlug: string): Promise<IUserProjectsRole | undefined> => {
|
fetchUserProjectPermissions = async (workspaceSlug: string): Promise<IUserProjectsRole> => {
|
||||||
try {
|
try {
|
||||||
const response = await workspaceService.getWorkspaceUserProjectsRole(workspaceSlug);
|
const response = await workspaceService.getWorkspaceUserProjectsRole(workspaceSlug);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
@@ -242,18 +295,17 @@ export class UserPermissionStore implements IUserPermissionStore {
|
|||||||
* @description Joins a project
|
* @description Joins a project
|
||||||
* @param { string } workspaceSlug
|
* @param { string } workspaceSlug
|
||||||
* @param { string } projectId
|
* @param { string } projectId
|
||||||
* @returns { Promise<void | undefined> }
|
* @returns { Promise<void> }
|
||||||
*/
|
*/
|
||||||
joinProject = async (workspaceSlug: string, projectId: string): Promise<void | undefined> => {
|
joinProject = async (workspaceSlug: string, projectId: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const response = await userService.joinProject(workspaceSlug, [projectId]);
|
const response = await userService.joinProject(workspaceSlug, [projectId]);
|
||||||
const projectMemberRole = this.workspaceInfoBySlug(workspaceSlug)?.role ?? EUserPermissions.MEMBER;
|
const projectMemberRole = this.getWorkspaceRoleByWorkspaceSlug(workspaceSlug) ?? EUserPermissions.MEMBER;
|
||||||
if (response) {
|
if (response) {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.workspaceProjectsPermissions, [workspaceSlug, projectId], projectMemberRole);
|
set(this.workspaceProjectsPermissions, [workspaceSlug, projectId], projectMemberRole);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return response;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error user joining the project", error);
|
console.error("Error user joining the project", error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -264,7 +316,7 @@ export class UserPermissionStore implements IUserPermissionStore {
|
|||||||
* @description Leaves a project
|
* @description Leaves a project
|
||||||
* @param { string } workspaceSlug
|
* @param { string } workspaceSlug
|
||||||
* @param { string } projectId
|
* @param { string } projectId
|
||||||
* @returns { Promise<void | undefined> }
|
* @returns { Promise<void> }
|
||||||
*/
|
*/
|
||||||
leaveProject = async (workspaceSlug: string, projectId: string): Promise<void> => {
|
leaveProject = async (workspaceSlug: string, projectId: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -1,23 +1,24 @@
|
|||||||
import cloneDeep from "lodash/cloneDeep";
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
import { action, makeObservable, observable, runInAction, computed } from "mobx";
|
import { action, makeObservable, observable, runInAction, computed } from "mobx";
|
||||||
// types
|
// plane imports
|
||||||
import { EUserPermissions } from "@plane/constants";
|
import { EUserPermissions } from "@plane/constants";
|
||||||
import { IUser } from "@plane/types";
|
import { IUser } from "@plane/types";
|
||||||
import { TUserPermissions } from "@plane/types/src/enums";
|
import { TUserPermissions } from "@plane/types/src/enums";
|
||||||
// constants
|
|
||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||||
// local
|
// local db
|
||||||
import { persistence } from "@/local-db/storage.sqlite";
|
import { persistence } from "@/local-db/storage.sqlite";
|
||||||
|
// plane web imports
|
||||||
|
import { RootStore } from "@/plane-web/store/root.store";
|
||||||
|
import { IUserPermissionStore, UserPermissionStore } from "@/plane-web/store/user/permission.store";
|
||||||
// services
|
// services
|
||||||
import { AuthService } from "@/services/auth.service";
|
import { AuthService } from "@/services/auth.service";
|
||||||
import { UserService } from "@/services/user.service";
|
import { UserService } from "@/services/user.service";
|
||||||
// stores
|
// stores
|
||||||
import { CoreRootStore } from "@/store/root.store";
|
|
||||||
import { IAccountStore } from "@/store/user/account.store";
|
import { IAccountStore } from "@/store/user/account.store";
|
||||||
import { ProfileStore, IUserProfileStore } from "@/store/user/profile.store";
|
import { ProfileStore, IUserProfileStore } from "@/store/user/profile.store";
|
||||||
import { IUserPermissionStore, UserPermissionStore } from "./permissions.store";
|
// local imports
|
||||||
import { IUserSettingsStore, UserSettingsStore } from "./settings.store";
|
import { IUserSettingsStore, UserSettingsStore } from "./settings.store";
|
||||||
|
|
||||||
type TUserErrorStatus = {
|
type TUserErrorStatus = {
|
||||||
@@ -68,7 +69,7 @@ export class UserStore implements IUserStore {
|
|||||||
userService: UserService;
|
userService: UserService;
|
||||||
authService: AuthService;
|
authService: AuthService;
|
||||||
|
|
||||||
constructor(private store: CoreRootStore) {
|
constructor(private store: RootStore) {
|
||||||
// stores
|
// stores
|
||||||
this.userProfile = new ProfileStore(store);
|
this.userProfile = new ProfileStore(store);
|
||||||
this.userSettings = new UserSettingsStore();
|
this.userSettings = new UserSettingsStore();
|
||||||
@@ -265,8 +266,7 @@ export class UserStore implements IUserStore {
|
|||||||
fetchProjectsWithCreatePermissions = (): { [key: string]: TUserPermissions } => {
|
fetchProjectsWithCreatePermissions = (): { [key: string]: TUserPermissions } => {
|
||||||
const { workspaceSlug } = this.store.router;
|
const { workspaceSlug } = this.store.router;
|
||||||
|
|
||||||
const allWorkspaceProjectRoles =
|
const allWorkspaceProjectRoles = this.permission.getProjectRolesByWorkspaceSlug(workspaceSlug || "");
|
||||||
this.permission.workspaceProjectsPermissions && this.permission.workspaceProjectsPermissions[workspaceSlug || ""];
|
|
||||||
|
|
||||||
const userPermissions =
|
const userPermissions =
|
||||||
(allWorkspaceProjectRoles &&
|
(allWorkspaceProjectRoles &&
|
||||||
|
|||||||
1
web/ee/helpers/project-settings.ts
Normal file
1
web/ee/helpers/project-settings.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "ce/helpers/project-settings";
|
||||||
1
web/ee/store/member/project-member.store.ts
Normal file
1
web/ee/store/member/project-member.store.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "ce/store/member/project-member.store";
|
||||||
1
web/ee/store/user/permission.store.ts
Normal file
1
web/ee/store/user/permission.store.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "ce/store/user/permission.store";
|
||||||
Reference in New Issue
Block a user