[WEB-2357] fix: update and redefine user roles across the platform (#5466)
* chore: removed viewer role * chore: indentation * chore: remove viewer role * chore: handled user permissions in store * chore: updated the migration file * chore: updated user permissions store * chore: removed the owner key * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor * fix: build error * chore: updated user permissions store and handled the permissions fetch in workspace and project wrappers * chore: package user enum updated * chore: user permission updated * chore: user permission updated * chore: resolved build errors * chore: resolved build error * chore: resolved build errors * chore: computedFn deep map issue resolved * chore: added back migration * chore: added new field in project table * chore: removed member store in users * chore: private project for admins * chore: workspace notification access validation updated * fix: workspace member edit option * fix: project intake permission validation updated * chore: workspace export settings permission updated * chore: guest_view_all_issues added * chore: guest_view_all_issues added * chore: key changed for guest access * chore: added validation for individual issues * chore: changed the dashboard issues count * chore: added new yarn file * chore: modified yarn file * chore: project page permission updated * chore: project page permission updated * chore: member setting ux updated * chore: build error * fix: yarn lock * fix: build error --------- Co-authored-by: gurusainath <gurusainath007@gmail.com> Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
This commit is contained in:
parent
7013a36629
commit
fdcd9a376c
172 changed files with 2057 additions and 1627 deletions
|
|
@ -24,7 +24,6 @@ import {
|
|||
import { Logo } from "@/components/common";
|
||||
import { ArchiveRestoreProjectModal, DeleteProjectModal, JoinProjectModal } from "@/components/project";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
|
|
@ -33,6 +32,8 @@ import { copyUrlToClipboard } from "@/helpers/string.helper";
|
|||
import { useProject } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane-web constants
|
||||
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
type Props = {
|
||||
project: IProject;
|
||||
|
|
@ -57,8 +58,8 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
|
|||
// derived values
|
||||
const projectMembersIds = project.members?.map((member) => member.member_id);
|
||||
// auth
|
||||
const isOwner = project.member_role === EUserProjectRoles.ADMIN;
|
||||
const isMember = project.member_role === EUserProjectRoles.MEMBER;
|
||||
const isOwner = project.member_role === EUserPermissions.ADMIN;
|
||||
const isMember = project.member_role === EUserPermissions.MEMBER;
|
||||
// archive
|
||||
const isArchived = !!project.archived_at;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,13 +8,12 @@ import { Search, Briefcase, X } from "lucide-react";
|
|||
import { Breadcrumbs, Button, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// constants
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useCommandPalette, useEventTracker, useProjectFilter, useUser } from "@/hooks/store";
|
||||
import { useCommandPalette, useEventTracker, useProjectFilter, useUserPermissions } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
import HeaderFilters from "./filters";
|
||||
|
||||
export const ProjectsBaseHeader = observer(() => {
|
||||
|
|
@ -25,9 +24,8 @@ export const ProjectsBaseHeader = observer(() => {
|
|||
// store hooks
|
||||
const { toggleCreateProjectModal } = useCommandPalette();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const { searchQuery, updateSearchQuery } = useProjectFilter();
|
||||
|
|
@ -37,7 +35,10 @@ export const ProjectsBaseHeader = observer(() => {
|
|||
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||
});
|
||||
// auth
|
||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
const isAuthorizedUser = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
const isArchived = pathname.includes("/archives");
|
||||
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type { IProject } from "@plane/types";
|
|||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// hooks
|
||||
import { useProject, useUser } from "@/hooks/store";
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
|
||||
// type
|
||||
|
|
@ -24,9 +24,7 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = (props) => {
|
|||
// states
|
||||
const [isJoiningLoading, setIsJoiningLoading] = useState(false);
|
||||
// store hooks
|
||||
const {
|
||||
membership: { joinProject },
|
||||
} = useUser();
|
||||
const { joinProject } = useUserPermissions();
|
||||
const { fetchProjects } = useProject();
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
|
|
@ -34,7 +32,7 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = (props) => {
|
|||
const handleJoin = () => {
|
||||
setIsJoiningLoading(true);
|
||||
|
||||
joinProject(workspaceSlug, [project.id])
|
||||
joinProject(workspaceSlug, project.id)
|
||||
.then(() => {
|
||||
router.push(`/${workspaceSlug}/projects/${project.id}/issues`);
|
||||
fetchProjects(workspaceSlug);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
|||
// constants
|
||||
import { PROJECT_MEMBER_LEAVE } from "@/constants/event-tracker";
|
||||
// hooks
|
||||
import { useEventTracker, useUser } from "@/hooks/store";
|
||||
import { useEventTracker, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
|
||||
type FormData = {
|
||||
|
|
@ -40,9 +40,7 @@ export const LeaveProjectModal: FC<ILeaveProjectModal> = observer((props) => {
|
|||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
const {
|
||||
membership: { leaveProject },
|
||||
} = useUser();
|
||||
const { leaveProject } = useUserPermissions();
|
||||
|
||||
const {
|
||||
control,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { ConfirmProjectMemberRemove } from "@/components/project";
|
|||
import { PROJECT_MEMBER_LEAVE } from "@/constants/event-tracker";
|
||||
|
||||
// hooks
|
||||
import { useEventTracker, useMember, useProject, useUser } from "@/hooks/store";
|
||||
import { useEventTracker, useMember, useProject, useUser, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useProjectColumns } from "@/plane-web/components/projects/settings/useProjectColumns";
|
||||
import { IProjectMemberDetails } from "@/store/member/project-member.store";
|
||||
|
|
@ -25,9 +25,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||
// router
|
||||
const router = useAppRouter();
|
||||
// store hooks
|
||||
const {
|
||||
membership: { leaveProject },
|
||||
} = useUser();
|
||||
const { leaveProject } = useUserPermissions();
|
||||
const { data: currentUser } = useUser();
|
||||
const { fetchProjects } = useProject();
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ import { Button } from "@plane/ui";
|
|||
import { ProjectMemberListItem, SendProjectInvitationModal } from "@/components/project";
|
||||
// ui
|
||||
import { MembersSettingsLoader } from "@/components/ui";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { useEventTracker, useMember, useUser } from "@/hooks/store";
|
||||
import { useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
|
||||
// plane-web constants
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
export const ProjectMemberList: React.FC = observer(() => {
|
||||
// states
|
||||
|
|
@ -21,9 +22,7 @@ export const ProjectMemberList: React.FC = observer(() => {
|
|||
const {
|
||||
project: { projectMemberIds, getProjectMemberDetails },
|
||||
} = useMember();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const searchedMembers = (projectMemberIds ?? []).filter((userId) => {
|
||||
const memberDetails = getProjectMemberDetails(userId);
|
||||
|
||||
|
|
@ -36,6 +35,8 @@ export const ProjectMemberList: React.FC = observer(() => {
|
|||
});
|
||||
const memberDetails = searchedMembers?.map((memberId) => getProjectMemberDetails(memberId));
|
||||
|
||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SendProjectInvitationModal isOpen={inviteModal} onClose={() => setInviteModal(false)} />
|
||||
|
|
@ -52,7 +53,7 @@ export const ProjectMemberList: React.FC = observer(() => {
|
|||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{currentProjectRole === EUserProjectRoles.ADMIN && (
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -7,14 +7,15 @@ import { Controller, useForm } from "react-hook-form";
|
|||
import useSWR from "swr";
|
||||
import { IProject, IUserLite, IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
import { Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Loader, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { MemberSelect } from "@/components/project";
|
||||
// constants
|
||||
import { PROJECT_MEMBERS } from "@/constants/fetch-keys";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// hooks
|
||||
import { useProject, useUser } from "@/hooks/store";
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
// types
|
||||
|
||||
const defaultValues: Partial<IProject> = {
|
||||
|
|
@ -26,12 +27,16 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
|
|||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
const { currentProjectDetails, fetchProjectDetails, updateProject } = useProject();
|
||||
// derived values
|
||||
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||
const isAdmin = allowPermissions(
|
||||
[EUserPermissions.ADMIN],
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
currentProjectDetails?.workspace_detail?.slug,
|
||||
currentProjectDetails?.id
|
||||
);
|
||||
// form info
|
||||
const { reset, control } = useForm<IProject>({ defaultValues });
|
||||
// fetching user members
|
||||
|
|
@ -83,6 +88,24 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
|
|||
});
|
||||
};
|
||||
|
||||
const toggleGuestViewAllIssues = async (value: boolean) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
updateProject(workspaceSlug.toString(), projectId.toString(), {
|
||||
guest_view_all_features: value,
|
||||
})
|
||||
.then(() => {
|
||||
setToast({
|
||||
title: "Success!",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
message: "Project updated successfully",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
||||
|
|
@ -142,6 +165,24 @@ export const ProjectSettingsMemberDefaults: React.FC = observer(() => {
|
|||
</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">
|
||||
Grant view access to all issues for guest users:
|
||||
</h3>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
This will allow guests to have view access to all the project issues.
|
||||
</p>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
value={currentProjectDetails?.guest_view_all_features}
|
||||
onChange={() => toggleGuestViewAllIssues(!currentProjectDetails?.guest_view_all_features)}
|
||||
disabled={!isAdmin}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@ import { useParams } from "next/navigation";
|
|||
import { useForm, Controller, useFieldArray } from "react-hook-form";
|
||||
import { ChevronDown, Plus, X } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
// ui
|
||||
import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// helpers
|
||||
import { PROJECT_MEMBER_ADDED } from "@/constants/event-tracker";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace";
|
||||
import { useEventTracker, useMember, useUser } from "@/hooks/store";
|
||||
// constants
|
||||
import { ROLE } from "@/constants/workspace";
|
||||
// hooks
|
||||
import { useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
|
||||
// plane-web constants
|
||||
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
|
|
@ -23,7 +23,7 @@ type Props = {
|
|||
};
|
||||
|
||||
type member = {
|
||||
role: EUserProjectRoles;
|
||||
role: EUserPermissions;
|
||||
member_id: string;
|
||||
};
|
||||
|
||||
|
|
@ -46,9 +46,7 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { projectUserInfo } = useUserPermissions();
|
||||
const {
|
||||
project: { projectMemberIds, bulkAddMembersToProject },
|
||||
workspace: { workspaceMemberIds, getWorkspaceMemberDetails },
|
||||
|
|
@ -68,6 +66,8 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||
name: "members",
|
||||
});
|
||||
|
||||
const currentProjectRole = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]?.role;
|
||||
|
||||
const uninvitedPeople = workspaceMemberIds?.filter((userId) => {
|
||||
const isInvited = projectMemberIds?.find((u) => u === userId);
|
||||
|
||||
|
|
@ -173,12 +173,10 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||
const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role;
|
||||
if (!value || !currentMemberWorkspaceRole) return ROLE;
|
||||
|
||||
const isGuestOrViewer = [EUserWorkspaceRoles.GUEST, EUserWorkspaceRoles.VIEWER].includes(
|
||||
currentMemberWorkspaceRole
|
||||
);
|
||||
const isGuest = [EUserPermissions.GUEST].includes(currentMemberWorkspaceRole);
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(ROLE).filter(([key]) => !isGuestOrViewer || [5, 10].includes(parseInt(key)))
|
||||
Object.entries(ROLE).filter(([key]) => !isGuest || [5].includes(parseInt(key)))
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -258,7 +256,7 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||
const newValue = ROLE[workspaceRole].toUpperCase();
|
||||
setValue(
|
||||
`members.${index}.role`,
|
||||
EUserProjectRoles[newValue as keyof typeof EUserProjectRoles]
|
||||
EUserPermissions[newValue as keyof typeof EUserPermissions]
|
||||
);
|
||||
}}
|
||||
options={options}
|
||||
|
|
@ -297,7 +295,7 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
|
|||
{Object.entries(
|
||||
checkCurrentOptionWorkspaceRole(watch(`members.${index}.member_id`))
|
||||
).map(([key, label]) => {
|
||||
if (parseInt(key) > (currentProjectRole ?? EUserProjectRoles.GUEST)) return null;
|
||||
if (parseInt(key) > (currentProjectRole ?? EUserPermissions.GUEST)) return null;
|
||||
|
||||
return (
|
||||
<CustomSelect.Option key={key} value={key}>
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import { Trash2 } from "lucide-react";
|
|||
import { Disclosure } from "@headlessui/react";
|
||||
import { IUser, IWorkspaceMember } from "@plane/types";
|
||||
import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace";
|
||||
import { ROLE } from "@/constants/workspace";
|
||||
import { useMember, useUser } from "@/hooks/store";
|
||||
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
export interface RowData {
|
||||
member: IWorkspaceMember;
|
||||
role: EUserWorkspaceRoles;
|
||||
role: EUserPermissions;
|
||||
}
|
||||
|
||||
type NameProps = {
|
||||
|
|
@ -24,7 +24,7 @@ type NameProps = {
|
|||
|
||||
type AccountTypeProps = {
|
||||
rowData: RowData;
|
||||
currentProjectRole: EUserProjectRoles | undefined;
|
||||
currentProjectRole: EUserPermissions | undefined;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
|
@ -97,16 +97,14 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
|
|||
|
||||
// derived values
|
||||
const isCurrentUser = currentUser?.id === rowData.member.id;
|
||||
const isAdminRole = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||
const isAdminRole = currentProjectRole === EUserPermissions.ADMIN;
|
||||
const isRoleNonEditable = isCurrentUser || !isAdminRole;
|
||||
|
||||
const checkCurrentOptionWorkspaceRole = (value: string) => {
|
||||
const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role;
|
||||
const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role as EUserPermissions | undefined;
|
||||
if (!value || !currentMemberWorkspaceRole) return ROLE;
|
||||
|
||||
const isGuestOrViewer = [EUserWorkspaceRoles.GUEST, EUserWorkspaceRoles.VIEWER].includes(
|
||||
currentMemberWorkspaceRole
|
||||
);
|
||||
const isGuestOrViewer = [EUserPermissions.GUEST].includes(currentMemberWorkspaceRole);
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(ROLE).filter(([key]) => !isGuestOrViewer || [5, 10].includes(parseInt(key)))
|
||||
|
|
@ -127,11 +125,11 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
|
|||
render={({ field: { value } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={(value: EUserProjectRoles) => {
|
||||
onChange={(value: EUserPermissions) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
updateMember(workspaceSlug.toString(), projectId.toString(), rowData.member.id, {
|
||||
role: value as unknown as EUserProjectRoles, // Cast value to unknown first, then to EUserWorkspaceRoles
|
||||
role: value as unknown as EUserPermissions, // Cast value to unknown first, then to EUserPermissions
|
||||
}).catch((err) => {
|
||||
console.log(err, "err");
|
||||
const error = err.error;
|
||||
|
|
@ -155,7 +153,7 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
|
|||
input
|
||||
>
|
||||
{Object.entries(checkCurrentOptionWorkspaceRole(rowData.member.id)).map(([key, label]) => {
|
||||
if (parseInt(key) > (currentProjectRole ?? EUserProjectRoles.GUEST)) return null;
|
||||
if (parseInt(key) > (currentProjectRole ?? EUserPermissions.GUEST)) return null;
|
||||
return (
|
||||
<CustomSelect.Option key={key} value={key}>
|
||||
{label}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue