[WEB-2126] chore: guest and viewer role permission (#5347)

* chore: user store code refactor

* chore: general unauthorized screen asset added

* chore: workspace setting sidebar options updated for guest and viewer

* chore: NotAuthorizedView component code updated

* chore: project setting layout code refactor

* chore: workspace setting members and exports page permission validation added

* chore: workspace members and exports settings page improvement

* chore: project invite modal updated

* chore: workspace setting unauthorized access empty state

* chore: workspace setting unauthorized access empty state

* chore: project settings sidebar permission updated

* fix: project settings user role permission updated

* chore: app sidebar role permission validation updated

* chore: app sidebar role permission validation

* chore: disabled page empty state validation

* chore: app sidebar add project improvement

* chore: guest role changes

* fix: user favorite

* chore: changed pages permission

* chore: guest role changes

* fix: app sidebar project item permission

* fix: project setting empty state flicker

* fix: workspace setting empty state flicker

* chore: granted notification permission to viewer

* chore: project invite and edit validation updated

* chore: favorite validation added for guest and viewer role

* chore: create view validation updated

* chore: views permission changes

* chore: create view empty state validation updated

* chore: created ENUM for permissions

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
This commit is contained in:
Anmol Singh Bhatia 2024-08-16 16:35:05 +05:30 committed by GitHub
parent d60e988ca1
commit 0a1c656865
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 957 additions and 590 deletions

View file

@ -6,7 +6,10 @@ import { useParams, useSearchParams } from "next/navigation";
import { TPageNavigationTabs } from "@plane/types";
// components
import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state";
import { PagesListRoot, PagesListView } from "@/components/pages";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// hooks
import { useProject } from "@/hooks/store";
@ -16,7 +19,7 @@ const ProjectPagesPage = observer(() => {
const type = searchParams.get("type");
const { workspaceSlug, projectId } = useParams();
// store hooks
const { getProjectById } = useProject();
const { getProjectById, currentProjectDetails } = useProject();
// derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Pages` : undefined;
@ -29,6 +32,17 @@ const ProjectPagesPage = observer(() => {
};
if (!workspaceSlug || !projectId) return <></>;
// No access to cycle
if (currentProjectDetails?.page_view === false)
return (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_PAGE}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
/>
</div>
);
return (
<>
<PageHead title={pageTitle} />

View file

@ -7,10 +7,9 @@ import { IProject } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automation";
import { PageHead } from "@/components/core";
// constants
import { EUserProjectRoles } from "@/constants/project";
// hooks
import { useProject, useUser } from "@/hooks/store";
@ -19,6 +18,7 @@ const AutomationSettingsPage = observer(() => {
const { workspaceSlug, projectId } = useParams();
// store hooks
const {
canPerformProjectAdminActions,
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails: projectDetails, updateProject } = useProject();
@ -36,13 +36,16 @@ const AutomationSettingsPage = observer(() => {
};
// derived values
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined;
if (currentProjectRole && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView />;
}
return (
<>
<PageHead title={pageTitle} />
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
<section className={`w-full overflow-y-auto py-8 pr-9 ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
<div className="flex items-center border-b border-custom-border-100 py-3.5">
<h3 className="text-xl font-medium">Automations</h3>
</div>

View file

@ -3,30 +3,40 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { EstimateRoot } from "@/components/estimates";
// constants
import { EUserProjectRoles } from "@/constants/project";
// hooks
import { useUser, useProject } from "@/hooks/store";
const EstimatesSettingsPage = observer(() => {
const { workspaceSlug, projectId } = useParams();
const {
canPerformProjectAdminActions,
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
// derived values
const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN;
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Estimates` : undefined;
if (!workspaceSlug || !projectId) return <></>;
if (currentProjectRole && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView />;
}
return (
<>
<PageHead title={pageTitle} />
<div className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "pointer-events-none opacity-60"}`}>
<EstimateRoot workspaceSlug={workspaceSlug?.toString()} projectId={projectId?.toString()} isAdmin={isAdmin} />
<div
className={`w-full overflow-y-auto py-8 pr-9 ${canPerformProjectAdminActions ? "" : "pointer-events-none opacity-60"}`}
>
<EstimateRoot
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
isAdmin={canPerformProjectAdminActions}
/>
</div>
</>
);

View file

@ -2,12 +2,10 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { ProjectFeaturesList } from "@/components/project";
// constants
import { EUserProjectRoles } from "@/constants/project";
// hooks
import { useProject, useUser } from "@/hooks/store";
@ -15,28 +13,27 @@ const FeaturesSettingsPage = observer(() => {
const { workspaceSlug, projectId } = useParams();
// store
const {
membership: { fetchUserProjectInfo },
canPerformProjectAdminActions,
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
// fetch the project details
const { data: memberDetails } = useSWR(
workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null
);
// derived values
const isAdmin = memberDetails?.role === EUserProjectRoles.ADMIN;
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined;
if (!workspaceSlug || !projectId) return null;
if (currentProjectRole && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView />;
}
return (
<>
<PageHead title={pageTitle} />
<section className={`w-full overflow-y-auto py-8 pr-9 ${isAdmin ? "" : "opacity-60"}`}>
<section className={`w-full overflow-y-auto py-8 pr-9 ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
<ProjectFeaturesList
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
isAdmin={isAdmin}
isAdmin={canPerformProjectAdminActions}
/>
</section>
</>

View file

@ -24,7 +24,7 @@ export const ProjectSettingHeader: FC = observer(() => {
} = useUser();
const { currentProjectDetails, loader } = useProject();
if (currentProjectRole && currentProjectRole <= EUserProjectRoles.VIEWER) return null;
const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST;
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
@ -70,14 +70,17 @@ export const ProjectSettingHeader: FC = observer(() => {
placement="bottom-start"
closeOnSelect
>
{PROJECT_SETTINGS_LINKS.map((item) => (
<CustomMenu.MenuItem
key={item.key}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)}
>
{item.label}
</CustomMenu.MenuItem>
))}
{PROJECT_SETTINGS_LINKS.map(
(item) =>
projectMemberInfo >= item.access && (
<CustomMenu.MenuItem
key={item.key}
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)}
>
{item.label}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
</div>

View file

@ -5,13 +5,19 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { ProjectSettingsLabelList } from "@/components/labels";
// hooks
import { useProject } from "@/hooks/store";
import { useProject, useUser } from "@/hooks/store";
const LabelsSettingsPage = observer(() => {
// store hooks
const { currentProjectDetails } = useProject();
const {
canPerformProjectMemberActions,
membership: { currentProjectRole },
} = useUser();
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined;
const scrollableContainerRef = useRef<HTMLDivElement | null>(null);
@ -29,6 +35,10 @@ const LabelsSettingsPage = observer(() => {
);
}, [scrollableContainerRef?.current]);
if (currentProjectRole && !canPerformProjectMemberActions) {
return <NotAuthorizedView section="settings" isProjectView />;
}
return (
<>
<PageHead title={pageTitle} />

View file

@ -1,18 +1,8 @@
"use client";
import { FC, ReactNode } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
// ui
import { Button, LayersIcon } from "@plane/ui";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { AppHeader, ContentWrapper } from "@/components/core";
// constants
import { EUserProjectRoles } from "@/constants/project";
// hooks
import { useUser } from "@/hooks/store";
// local components
import { ProjectSettingHeader } from "./header";
import { ProjectSettingsSidebar } from "./sidebar";
@ -21,33 +11,8 @@ export interface IProjectSettingLayout {
children: ReactNode;
}
const ProjectSettingLayout: FC<IProjectSettingLayout> = observer((props) => {
const ProjectSettingLayout: FC<IProjectSettingLayout> = (props) => {
const { children } = props;
// router
const { workspaceSlug, projectId } = useParams();
// store hooks
const {
membership: { currentProjectRole },
} = useUser();
const restrictViewSettings = currentProjectRole && currentProjectRole <= EUserProjectRoles.VIEWER;
if (restrictViewSettings) {
return (
<NotAuthorizedView
type="project"
actionButton={
//TODO: Create a new component called Button Link to handle such scenarios
<Link href={`/${workspaceSlug}/projects/${projectId}/issues`}>
<Button variant="primary" size="md" prependIcon={<LayersIcon />}>
Go to issues
</Button>
</Link>
}
/>
);
}
return (
<>
<AppHeader header={<ProjectSettingHeader />} />
@ -63,6 +28,6 @@ const ProjectSettingLayout: FC<IProjectSettingLayout> = observer((props) => {
</ContentWrapper>
</>
);
});
};
export default ProjectSettingLayout;

View file

@ -2,17 +2,26 @@
import { observer } from "mobx-react";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { ProjectMemberList, ProjectSettingsMemberDefaults } from "@/components/project";
// hooks
import { useProject } from "@/hooks/store";
import { useProject, useUser } from "@/hooks/store";
const MembersSettingsPage = observer(() => {
// store
const { currentProjectDetails } = useProject();
const {
canPerformProjectViewerActions,
membership: { currentProjectRole },
} = useUser();
// derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined;
if (currentProjectRole && !canPerformProjectViewerActions) {
return <NotAuthorizedView section="settings" isProjectView />;
}
return (
<>
<PageHead title={pageTitle} />

View file

@ -1,6 +1,7 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
// ui
@ -12,7 +13,7 @@ import { EUserProjectRoles, PROJECT_SETTINGS_LINKS } from "@/constants/project";
// hooks
import { useUser } from "@/hooks/store";
export const ProjectSettingsSidebar = () => {
export const ProjectSettingsSidebar = observer(() => {
const { workspaceSlug, projectId } = useParams();
const pathname = usePathname();
// mobx store
@ -60,4 +61,4 @@ export const ProjectSettingsSidebar = () => {
</div>
</div>
);
};
});

View file

@ -3,18 +3,27 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { ProjectStateRoot } from "@/components/project-states";
// hook
import { useProject } from "@/hooks/store";
import { useProject, useUser } from "@/hooks/store";
const StatesSettingsPage = observer(() => {
const { workspaceSlug, projectId } = useParams();
// store
const { currentProjectDetails } = useProject();
const {
canPerformProjectMemberActions,
membership: { currentProjectRole },
} = useUser();
// derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined;
if (currentProjectRole && !canPerformProjectMemberActions) {
return <NotAuthorizedView section="settings" isProjectView />;
}
return (
<>
<PageHead title={pageTitle} />

View file

@ -12,21 +12,17 @@ import { BreadcrumbLink, Logo } from "@/components/common";
import { ViewListHeader } from "@/components/views";
import { ViewAppliedFiltersList } from "@/components/views/applied-filters";
// constants
import { EUserProjectRoles } from "@/constants/project";
import { EViewAccess } from "@/constants/views";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useCommandPalette, useProject, useProjectView, useUser } from "@/hooks/store";
import { useCommandPalette, useProject, useProjectView } from "@/hooks/store";
export const ProjectViewsHeader = observer(() => {
// router
const { workspaceSlug } = useParams();
// store hooks
const { toggleCreateViewModal } = useCommandPalette();
const {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails, loader } = useProject();
const { filters, updateFilters, clearAllFilters } = useProjectView();
@ -49,9 +45,6 @@ export const ProjectViewsHeader = observer(() => {
const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0;
const canUserCreateView =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
return (
<>
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
@ -83,13 +76,11 @@ export const ProjectViewsHeader = observer(() => {
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<ViewListHeader />
{canUserCreateView && (
<div>
<Button variant="primary" size="sm" onClick={() => toggleCreateViewModal(true)}>
Add view
</Button>
</div>
)}
<div>
<Button variant="primary" size="sm" onClick={() => toggleCreateViewModal(true)}>
Add view
</Button>
</div>
</div>
</div>
{isFiltersApplied && (

View file

@ -8,13 +8,13 @@ import useSWR from "swr";
import { Button } from "@plane/ui";
// component
import { ApiTokenListItem, CreateApiTokenModal } from "@/components/api-token";
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state";
import { APITokenSettingsLoader } from "@/components/ui";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// store hooks
import { useUser, useWorkspace } from "@/hooks/store";
// services
@ -29,27 +29,22 @@ const ApiTokensPage = observer(() => {
const { workspaceSlug } = useParams();
// store hooks
const {
canPerformWorkspaceAdminActions,
membership: { currentWorkspaceRole },
} = useUser();
const { currentWorkspace } = useWorkspace();
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
const { data: tokens } = useSWR(workspaceSlug && isAdmin ? API_TOKENS_LIST(workspaceSlug.toString()) : null, () =>
workspaceSlug && isAdmin ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null
const { data: tokens } = useSWR(
workspaceSlug && canPerformWorkspaceAdminActions ? API_TOKENS_LIST(workspaceSlug.toString()) : null,
() =>
workspaceSlug && canPerformWorkspaceAdminActions ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null
);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - API Tokens` : undefined;
if (!isAdmin)
return (
<>
<PageHead title={pageTitle} />
<div className="mt-10 flex h-full w-full justify-center p-4">
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
</div>
</>
);
if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) {
return <NotAuthorizedView section="settings" />;
}
if (!tokens) {
return <APITokenSettingsLoader />;
@ -92,4 +87,4 @@ const ApiTokensPage = observer(() => {
);
});
export default ApiTokensPage;
export default ApiTokensPage;

View file

@ -2,9 +2,8 @@
import { observer } from "mobx-react";
// component
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks
import { useUser, useWorkspace } from "@/hooks/store";
// plane web components
@ -13,22 +12,16 @@ import { BillingRoot } from "@/plane-web/components/workspace";
const BillingSettingsPage = observer(() => {
// store hooks
const {
canPerformWorkspaceAdminActions,
membership: { currentWorkspaceRole },
} = useUser();
const { currentWorkspace } = useWorkspace();
// derived values
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined;
if (!isAdmin)
return (
<>
<PageHead title={pageTitle} />
<div className="mt-10 flex h-full w-full justify-center p-4">
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
</div>
</>
);
if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) {
return <NotAuthorizedView section="settings" />;
}
return (
<>

View file

@ -2,39 +2,39 @@
import { observer } from "mobx-react";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import ExportGuide from "@/components/exporter/guide";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useUser, useWorkspace } from "@/hooks/store";
const ExportsPage = observer(() => {
// store hooks
const {
canPerformWorkspaceViewerActions,
canPerformWorkspaceMemberActions,
membership: { currentWorkspaceRole },
} = useUser();
const { currentWorkspace } = useWorkspace();
// derived values
const hasPageAccess =
currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Exports` : undefined;
if (!hasPageAccess)
return (
<>
<PageHead title={pageTitle} />
<div className="mt-10 flex h-full w-full justify-center p-4">
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
</div>
</>
);
// if user is not authorized to view this page
if (currentWorkspaceRole && !canPerformWorkspaceViewerActions) {
return <NotAuthorizedView section="settings" />;
}
return (
<>
<PageHead title={pageTitle} />
<div className="w-full overflow-y-auto md:pr-9 pr-4">
<div
className={cn("w-full overflow-y-auto md:pr-9 pr-4", {
"opacity-60": !canPerformWorkspaceMemberActions,
})}
>
<div className="flex items-center border-b border-custom-border-100 py-3.5">
<h3 className="text-xl font-medium">Exports</h3>
</div>
@ -44,4 +44,4 @@ const ExportsPage = observer(() => {
);
});
export default ExportsPage;
export default ExportsPage;

View file

@ -9,12 +9,13 @@ import { IWorkspaceBulkInviteFormData } from "@plane/types";
// ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "@/components/workspace";
// constants
import { MEMBER_INVITED } from "@/constants/event-tracker";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
import { cn } from "@/helpers/common.helper";
import { getUserRole } from "@/helpers/user.helper";
// hooks
import { useEventTracker, useMember, useUser, useWorkspace } from "@/hooks/store";
@ -28,6 +29,9 @@ const WorkspaceMembersSettingsPage = observer(() => {
// store hooks
const { captureEvent } = useEventTracker();
const {
canPerformWorkspaceAdminActions,
canPerformWorkspaceViewerActions,
canPerformWorkspaceMemberActions,
membership: { currentWorkspaceRole },
} = useUser();
const {
@ -79,9 +83,13 @@ const WorkspaceMembersSettingsPage = observer(() => {
};
// derived values
const isAdmin = currentWorkspaceRole && [EUserWorkspaceRoles.ADMIN].includes(currentWorkspaceRole);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined;
// if user is not authorized to view this page
if (currentWorkspaceRole && !canPerformWorkspaceViewerActions) {
return <NotAuthorizedView section="settings" />;
}
return (
<>
<PageHead title={pageTitle} />
@ -90,7 +98,11 @@ const WorkspaceMembersSettingsPage = observer(() => {
onClose={() => setInviteModal(false)}
onSubmit={handleWorkspaceInvite}
/>
<section className="w-full overflow-y-auto md:pr-9 pr-4">
<section
className={cn("w-full overflow-y-auto md:pr-9 pr-4", {
"opacity-60": !canPerformWorkspaceMemberActions,
})}
>
<div className="flex items-center justify-between gap-4 py-3.5">
<h4 className="text-xl font-medium">Members</h4>
<div className="ml-auto flex items-center gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5">
@ -103,13 +115,13 @@ const WorkspaceMembersSettingsPage = observer(() => {
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{isAdmin && (
{canPerformWorkspaceAdminActions && (
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
Add member
</Button>
)}
</div>
<WorkspaceMembersList searchQuery={searchQuery} isAdmin={isAdmin ?? false} />
<WorkspaceMembersList searchQuery={searchQuery} isAdmin={canPerformWorkspaceAdminActions} />
</section>
</>
);

View file

@ -7,6 +7,7 @@ import useSWR from "swr";
// ui
import { Button } from "@plane/ui";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
import { PageHead } from "@/components/core";
import { EmptyState } from "@/components/empty-state";
import { WebhookSettingsLoader } from "@/components/ui";
@ -23,16 +24,15 @@ const WebhooksListPage = observer(() => {
const { workspaceSlug } = useParams();
// mobx store
const {
canPerformWorkspaceAdminActions,
membership: { currentWorkspaceRole },
} = useUser();
const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook();
const { currentWorkspace } = useWorkspace();
const isAdmin = currentWorkspaceRole === 20;
useSWR(
workspaceSlug && isAdmin ? `WEBHOOKS_LIST_${workspaceSlug}` : null,
workspaceSlug && isAdmin ? () => fetchWebhooks(workspaceSlug.toString()) : null
workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null,
workspaceSlug && canPerformWorkspaceAdminActions ? () => fetchWebhooks(workspaceSlug.toString()) : null
);
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhooks` : undefined;
@ -42,15 +42,9 @@ const WebhooksListPage = observer(() => {
if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey();
}, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]);
if (!isAdmin)
return (
<>
<PageHead title={pageTitle} />
<div className="mt-10 flex h-full w-full justify-center p-4">
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
</div>
</>
);
if (currentWorkspaceRole && !canPerformWorkspaceAdminActions) {
return <NotAuthorizedView section="settings" />;
}
if (!webhooks) return <WebhookSettingsLoader />;
@ -95,4 +89,4 @@ const WebhooksListPage = observer(() => {
);
});
export default WebhooksListPage;
export default WebhooksListPage;

View file

@ -13,7 +13,7 @@ import {
import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu";
import { cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme } from "@/hooks/store";
import { useAppTheme, useUser } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
// plane web components
import useSize from "@/hooks/use-window-size";
@ -23,6 +23,7 @@ export interface IAppSidebar {}
export const AppSidebar: FC<IAppSidebar> = observer(() => {
// store hooks
const { canPerformWorkspaceMemberActions } = useUser();
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const windowSize = useSize();
// refs
@ -85,7 +86,7 @@ export const AppSidebar: FC<IAppSidebar> = observer(() => {
"opacity-0": !sidebarCollapsed,
})}
/>
<SidebarFavoritesMenu />
{canPerformWorkspaceMemberActions && <SidebarFavoritesMenu />}
<SidebarProjectsList />
</div>

View file

@ -14,11 +14,10 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/com
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
// constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import { useLabel, useMember, useUser, useIssues, useGlobalView } from "@/hooks/store";
import { useLabel, useMember, useIssues, useGlobalView } from "@/hooks/store";
export const GlobalIssuesHeader = observer(() => {
// states
@ -30,9 +29,6 @@ export const GlobalIssuesHeader = observer(() => {
issuesFilter: { filters, updateFilters },
} = useIssues(EIssuesStoreType.GLOBAL);
const { getViewDetailsById } = useGlobalView();
const {
membership: { currentWorkspaceRole },
} = useUser();
const { workspaceLabels } = useLabel();
const {
workspace: { workspaceMemberIds },
@ -97,8 +93,6 @@ export const GlobalIssuesHeader = observer(() => {
[workspaceSlug, updateFilters, globalViewId]
);
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const isLocked = viewDetails?.is_locked;
return (
@ -142,11 +136,10 @@ export const GlobalIssuesHeader = observer(() => {
</FiltersDropdown>
</>
)}
{isAuthorizedUser && (
<Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}>
Add view
</Button>
)}
<Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}>
Add view
</Button>
</div>
</div>
</>

View file

@ -17,7 +17,7 @@ export const WORKSPACE_SETTINGS = {
key: "members",
label: "Members",
href: `/settings/members`,
access: EUserWorkspaceRoles.GUEST,
access: EUserWorkspaceRoles.VIEWER,
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`,
Icon: SettingIcon,
},
@ -33,7 +33,7 @@ export const WORKSPACE_SETTINGS = {
key: "export",
label: "Exports",
href: `/settings/exports`,
access: EUserWorkspaceRoles.MEMBER,
access: EUserWorkspaceRoles.VIEWER,
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`,
Icon: SettingIcon,
},

View file

@ -1,62 +1,33 @@
import React from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
// hooks
import { useUser } from "@/hooks/store";
// layouts
import DefaultLayout from "@/layouts/default-layout";
// images
import ProjectNotAuthorizedImg from "@/public/auth/project-not-authorized.svg";
import Unauthorized from "@/public/auth/unauthorized.svg";
import WorkspaceNotAuthorizedImg from "@/public/auth/workspace-not-authorized.svg";
type Props = {
actionButton?: React.ReactNode;
type: "project" | "workspace";
section?: "settings" | "general";
isProjectView?: boolean;
};
export const NotAuthorizedView: React.FC<Props> = observer((props) => {
const { actionButton, type } = props;
// router
const searchParams = useSearchParams();
const next_path = searchParams.get("next_path");
// hooks
const { data: currentUser } = useUser();
const { actionButton, section = "general", isProjectView = false } = props;
// assets
const settingAsset = isProjectView ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg;
const asset = section === "settings" ? settingAsset : Unauthorized;
return (
<DefaultLayout>
<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">
<Image
src={type === "project" ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg}
height="176"
width="288"
alt="ProjectSettingImg"
/>
<Image src={asset} height="176" width="288" alt="ProjectSettingImg" />
</div>
<h1 className="text-xl font-medium text-custom-text-100">Oops! You are not authorized to view this page</h1>
<div className="w-full max-w-md text-base text-custom-text-200">
{currentUser ? (
<p>
You have signed in as {currentUser.email}. <br />
<Link href={`/?next_path=${next_path}`}>
<span className="font-medium text-custom-text-100">Sign in</span>
</Link>{" "}
with different account that has access to this page.
</p>
) : (
<p>
You need to{" "}
<Link href={`/?next_path=${next_path}`}>
<span className="font-medium text-custom-text-100">Sign in</span>
</Link>{" "}
with an account that has access to this page.
</p>
)}
</div>
{actionButton}
</div>
</DefaultLayout>

View file

@ -43,8 +43,8 @@ export const CommandPalette: FC = observer(() => {
const { platform } = usePlatformOS();
const {
data: currentUser,
canPerformProjectCreateActions,
canPerformWorkspaceCreateActions,
canPerformProjectMemberActions,
canPerformWorkspaceMemberActions,
canPerformAnyCreateAction,
canPerformProjectAdminActions,
} = useUser();
@ -103,15 +103,15 @@ export const CommandPalette: FC = observer(() => {
// auth
const performProjectCreateActions = useCallback(
(showToast: boolean = true) => {
if (!canPerformProjectCreateActions && showToast)
if (!canPerformProjectMemberActions && showToast)
setToast({
type: TOAST_TYPE.ERROR,
title: "You don't have permission to perform this action.",
});
return canPerformProjectCreateActions;
return canPerformProjectMemberActions;
},
[canPerformProjectCreateActions]
[canPerformProjectMemberActions]
);
const performProjectBulkDeleteActions = useCallback(
@ -129,14 +129,14 @@ export const CommandPalette: FC = observer(() => {
const performWorkspaceCreateActions = useCallback(
(showToast: boolean = true) => {
if (!canPerformWorkspaceCreateActions && showToast)
if (!canPerformWorkspaceMemberActions && showToast)
setToast({
type: TOAST_TYPE.ERROR,
title: "You don't have permission to perform this action.",
});
return canPerformWorkspaceCreateActions;
return canPerformWorkspaceMemberActions;
},
[canPerformWorkspaceCreateActions]
[canPerformWorkspaceMemberActions]
);
const performAnyProjectCreateActions = useCallback(

View file

@ -7,10 +7,12 @@ import { Earth, Info, Lock, Minus } from "lucide-react";
import { Avatar, FavoriteStar, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
// components
import { PageQuickActions } from "@/components/pages/dropdowns";
// constants
import { EUserProjectRoles } from "@/constants/project";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
// hooks
import { useMember, usePage } from "@/hooks/store";
import { useMember, usePage, useProject } from "@/hooks/store";
type Props = {
workspaceSlug: string;
@ -25,10 +27,15 @@ export const BlockItemAction: FC<Props> = observer((props) => {
// store hooks
const page = usePage(pageId);
const { getUserDetails } = useMember();
const { getProjectById } = useProject();
// derived values
const { access, created_at, is_favorite, owned_by, addToFavorites, removePageFromFavorites } = page;
// derived values
const project = getProjectById(projectId);
const isViewerOrGuest =
project?.member_role && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(project.member_role);
const ownerDetails = owned_by ? getUserDetails(owned_by) : undefined;
// handlers
@ -74,14 +81,16 @@ export const BlockItemAction: FC<Props> = observer((props) => {
</Tooltip>
{/* favorite/unfavorite */}
<FavoriteStar
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleFavorites();
}}
selected={is_favorite}
/>
{!isViewerOrGuest && (
<FavoriteStar
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleFavorites();
}}
selected={is_favorite}
/>
)}
{/* quick actions dropdown */}
<PageQuickActions

View file

@ -12,7 +12,7 @@ import { Avatar, Button, CustomSelect, CustomSearchSelect, TOAST_TYPE, setToast
// helpers
import { PROJECT_MEMBER_ADDED } from "@/constants/event-tracker";
import { EUserProjectRoles } from "@/constants/project";
import { ROLE } from "@/constants/workspace";
import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace";
import { useEventTracker, useMember, useUser } from "@/hooks/store";
// constants
@ -56,6 +56,8 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
// form info
const {
formState: { errors, isSubmitting },
watch,
setValue,
reset,
handleSubmit,
control,
@ -167,6 +169,19 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
}[]
| undefined;
const checkCurrentOptionWorkspaceRole = (value: string) => {
const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role;
if (!value || !currentMemberWorkspaceRole) return ROLE;
const isGuestOrViewer = [EUserWorkspaceRoles.GUEST, EUserWorkspaceRoles.VIEWER].includes(
currentMemberWorkspaceRole
);
return Object.fromEntries(
Object.entries(ROLE).filter(([key]) => !isGuestOrViewer || [5, 10].includes(parseInt(key)))
);
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
@ -237,6 +252,14 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
}
onChange={(val: string) => {
onChange(val);
// Update the role to the workspace role when member ID changes
const workspaceMemberDetails = getWorkspaceMemberDetails(val);
const workspaceRole = workspaceMemberDetails?.role ?? 5;
const newValue = ROLE[workspaceRole].toUpperCase();
setValue(
`members.${index}.role`,
EUserProjectRoles[newValue as keyof typeof EUserProjectRoles]
);
}}
options={options}
optionsClassName="w-full"
@ -271,7 +294,9 @@ export const SendProjectInvitationModal: React.FC<Props> = observer((props) => {
input
optionsClassName="w-full"
>
{Object.entries(ROLE).map(([key, label]) => {
{Object.entries(
checkCurrentOptionWorkspaceRole(watch(`members.${index}.member_id`))
).map(([key, label]) => {
if (parseInt(key) > (currentProjectRole ?? EUserProjectRoles.GUEST)) return null;
return (

View file

@ -91,6 +91,7 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
// store hooks
const {
project: { updateMember },
workspace: { getWorkspaceMemberDetails },
} = useMember();
const { data: currentUser } = useUser();
@ -99,6 +100,19 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
const isAdminRole = currentProjectRole === EUserProjectRoles.ADMIN;
const isRoleNonEditable = isCurrentUser || !isAdminRole;
const checkCurrentOptionWorkspaceRole = (value: string) => {
const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role;
if (!value || !currentMemberWorkspaceRole) return ROLE;
const isGuestOrViewer = [EUserWorkspaceRoles.GUEST, EUserWorkspaceRoles.VIEWER].includes(
currentMemberWorkspaceRole
);
return Object.fromEntries(
Object.entries(ROLE).filter(([key]) => !isGuestOrViewer || [5, 10].includes(parseInt(key)))
);
};
return (
<>
{isRoleNonEditable ? (
@ -140,11 +154,14 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
optionsClassName="w-full"
input
>
{Object.keys(ROLE).map((item) => (
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
{ROLE[item as unknown as keyof typeof ROLE]}
</CustomSelect.Option>
))}
{Object.entries(checkCurrentOptionWorkspaceRole(rowData.member.id)).map(([key, label]) => {
if (parseInt(key) > (currentProjectRole ?? EUserProjectRoles.GUEST)) return null;
return (
<CustomSelect.Option key={key} value={key}>
{label}
</CustomSelect.Option>
);
})}
</CustomSelect>
)}
/>

View file

@ -45,7 +45,7 @@ import { EUserProjectRoles } from "@/constants/project";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme, useEventTracker, useProject } from "@/hooks/store";
import { useAppTheme, useEventTracker, useProject, useUser } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import { usePlatformOS } from "@/hooks/use-platform-os";
// constants
@ -70,31 +70,37 @@ const navigation = (workspaceSlug: string, projectId: string) => [
name: "Issues",
href: `/${workspaceSlug}/projects/${projectId}/issues`,
Icon: LayersIcon,
access: EUserProjectRoles.GUEST,
},
{
name: "Cycles",
href: `/${workspaceSlug}/projects/${projectId}/cycles`,
Icon: ContrastIcon,
access: EUserProjectRoles.VIEWER,
},
{
name: "Modules",
href: `/${workspaceSlug}/projects/${projectId}/modules`,
Icon: DiceIcon,
access: EUserProjectRoles.VIEWER,
},
{
name: "Views",
href: `/${workspaceSlug}/projects/${projectId}/views`,
Icon: Layers,
access: EUserProjectRoles.GUEST,
},
{
name: "Pages",
href: `/${workspaceSlug}/projects/${projectId}/pages`,
Icon: FileText,
access: EUserProjectRoles.VIEWER,
},
{
name: "Intake",
href: `/${workspaceSlug}/projects/${projectId}/inbox`,
Icon: Intake,
access: EUserProjectRoles.GUEST,
},
];
@ -106,6 +112,9 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
const { setTrackElement } = useEventTracker();
const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject();
const { isMobile } = usePlatformOS();
const {
membership: { currentWorkspaceAllProjectsRole },
} = useUser();
// states
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
const [publishModalOpen, setPublishModal] = useState(false);
@ -378,16 +387,20 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
customButtonClassName="grid place-items-center"
placement="bottom-start"
>
<CustomMenu.MenuItem onClick={project.is_favorite ? handleRemoveFromFavorites : handleAddToFavorites}>
<span className="flex items-center justify-start gap-2">
<Star
className={cn("h-3.5 w-3.5 ", {
"fill-yellow-500 stroke-yellow-500": project.is_favorite,
})}
/>
<span>{project.is_favorite ? "Remove from favorites" : "Add to favorites"}</span>
</span>
</CustomMenu.MenuItem>
{!isViewerOrGuest && (
<CustomMenu.MenuItem
onClick={project.is_favorite ? handleRemoveFromFavorites : handleAddToFavorites}
>
<span className="flex items-center justify-start gap-2">
<Star
className={cn("h-3.5 w-3.5 ", {
"fill-yellow-500 stroke-yellow-500": project.is_favorite,
})}
/>
<span>{project.is_favorite ? "Remove from favorites" : "Add to favorites"}</span>
</span>
</CustomMenu.MenuItem>
)}
{/* publish project settings */}
{isAdmin && (
@ -400,14 +413,16 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem>
<Link href={`/${workspaceSlug}/projects/${project?.id}/draft-issues/`}>
<div className="flex items-center justify-start gap-2">
<PenSquare className="h-3.5 w-3.5 stroke-[1.5] text-custom-text-300" />
<span>Draft issues</span>
</div>
</Link>
</CustomMenu.MenuItem>
{!isViewerOrGuest && (
<CustomMenu.MenuItem>
<Link href={`/${workspaceSlug}/projects/${project?.id}/draft-issues/`}>
<div className="flex items-center justify-start gap-2">
<PenSquare className="h-3.5 w-3.5 stroke-[1.5] text-custom-text-300" />
<span>Draft issues</span>
</div>
</Link>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-3.5 w-3.5 stroke-[1.5]" />
@ -482,31 +497,37 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
(item.name === "Intake" && !project.inbox_view)
)
return;
const currentRole = currentWorkspaceAllProjectsRole
? currentWorkspaceAllProjectsRole[projectId]
: undefined;
return (
<Tooltip
key={item.name}
isMobile={isMobile}
tooltipContent={`${project?.name}: ${item.name}`}
position="right"
className="ml-2"
disabled={!isSidebarCollapsed}
>
<Link key={item.name} href={item.href} onClick={handleProjectClick}>
<SidebarNavItem
<>
{currentRole >= item.access && (
<Tooltip
key={item.name}
className={`pl-[18px] ${isSidebarCollapsed ? "p-0 size-7 justify-center mx-auto" : ""}`}
isActive={pathname.includes(item.href)}
isMobile={isMobile}
tooltipContent={`${project?.name}: ${item.name}`}
position="right"
className="ml-2"
disabled={!isSidebarCollapsed}
>
<div className="flex items-center gap-1.5 py-[1px]">
<item.Icon
className={`flex-shrink-0 size-4 ${item.name === "Intake" ? "stroke-1" : "stroke-[1.5]"}`}
/>
{!isSidebarCollapsed && <span className="text-xs font-medium">{item.name}</span>}
</div>
</SidebarNavItem>
</Link>
</Tooltip>
<Link key={item.name} href={item.href} onClick={handleProjectClick}>
<SidebarNavItem
key={item.name}
className={`pl-[18px] ${isSidebarCollapsed ? "p-0 size-7 justify-center mx-auto" : ""}`}
isActive={pathname.includes(item.href)}
>
<div className="flex items-center gap-1.5 py-[1px]">
<item.Icon
className={`flex-shrink-0 size-4 ${item.name === "Intake" ? "stroke-1" : "stroke-[1.5]"}`}
/>
{!isSidebarCollapsed && <span className="text-xs font-medium">{item.name}</span>}
</div>
</SidebarNavItem>
</Link>
</Tooltip>
)}
</>
);
})}
</Disclosure.Panel>

View file

@ -263,7 +263,6 @@ export const SidebarProjectsList: FC = observer(() => {
toggleCreateProjectModal(true);
}}
>
<Plus className="flex-shrink-0 size-4" />
{!isCollapsed && "Add project"}
</button>
)}

View file

@ -279,7 +279,7 @@ export const SIDEBAR_WORKSPACE_MENU_ITEMS: {
key: "active-cycles",
label: "Cycles",
href: `/active-cycles`,
access: EUserWorkspaceRoles.GUEST,
access: EUserWorkspaceRoles.MEMBER,
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`,
Icon: ContrastIcon,
},
@ -317,7 +317,7 @@ export const SIDEBAR_USER_MENU_ITEMS: {
key: "your-work",
label: "Your work",
href: "/profile",
access: EUserWorkspaceRoles.GUEST,
access: EUserWorkspaceRoles.MEMBER,
highlight: (pathname: string, baseUrl: string, options?: TLinkOptions) =>
options?.userId ? pathname.includes(`${baseUrl}/profile/${options?.userId}`) : false,
Icon: UserActivityIcon,

View file

@ -490,7 +490,7 @@ const emptyStateDetails = {
},
},
accessType: "project",
access: EUserProjectRoles.MEMBER,
access: EUserProjectRoles.GUEST,
},
// project pages
[EmptyStateType.PROJECT_PAGE]: {

View file

@ -79,7 +79,7 @@ export const PROJECT_SETTINGS_LINKS: {
key: "general",
label: "General",
href: `/settings`,
access: EUserProjectRoles.MEMBER,
access: EUserProjectRoles.GUEST,
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`,
Icon: SettingIcon,
},
@ -87,7 +87,7 @@ export const PROJECT_SETTINGS_LINKS: {
key: "members",
label: "Members",
href: `/settings/members`,
access: EUserProjectRoles.MEMBER,
access: EUserProjectRoles.VIEWER,
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`,
Icon: SettingIcon,
},

View file

@ -6,7 +6,7 @@ import {
import { useProject, usePage, useProjectView, useCycle, useModule } from "@/hooks/store";
export const useFavoriteItemDetails = (workspaceSlug: string, favorite: IFavorite) => {
const favoriteItemId = favorite.entity_data.id;
const favoriteItemId = favorite?.entity_data?.id;
const favoriteItemLogoProps = favorite?.entity_data?.logo_props;
const favoriteItemName = favorite?.entity_data.name || favorite?.name;
const favoriteItemEntityType = favorite?.entity_type;

View file

@ -30,7 +30,7 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
// next themes
const { resolvedTheme } = useTheme();
// store hooks
const { membership, signOut, data: currentUser } = useUser();
const { membership, signOut, data: currentUser, canPerformWorkspaceMemberActions } = useUser();
const { fetchProjects } = useProject();
const { fetchFavorite } = useFavorite();
const {
@ -72,8 +72,12 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
);
// fetch workspace favorite
useSWR(
workspaceSlug && currentWorkspace ? `WORKSPACE_FAVORITE_${workspaceSlug}` : null,
workspaceSlug && currentWorkspace ? () => fetchFavorite(workspaceSlug.toString()) : null,
workspaceSlug && currentWorkspace && canPerformWorkspaceMemberActions
? `WORKSPACE_FAVORITE_${workspaceSlug}`
: null,
workspaceSlug && currentWorkspace && canPerformWorkspaceMemberActions
? () => fetchFavorite(workspaceSlug.toString())
: null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);

View file

@ -42,9 +42,18 @@ export interface IUserStore {
reset: () => void;
signOut: () => Promise<void>;
// computed
canPerformProjectCreateActions: boolean;
// workspace level
canPerformWorkspaceAdminActions: boolean;
canPerformWorkspaceMemberActions: boolean;
canPerformWorkspaceViewerActions: boolean;
canPerformWorkspaceGuestActions: boolean;
// project level
canPerformProjectAdminActions: boolean;
canPerformWorkspaceCreateActions: boolean;
canPerformProjectMemberActions: boolean;
canPerformProjectViewerActions: boolean;
canPerformProjectGuestActions: boolean;
canPerformAnyCreateAction: boolean;
projectsWithCreatePermissions: { [projectId: string]: number } | null;
}
@ -92,9 +101,16 @@ export class UserStore implements IUserStore {
reset: action,
signOut: action,
// computed
canPerformProjectCreateActions: computed,
canPerformWorkspaceAdminActions: computed,
canPerformWorkspaceMemberActions: computed,
canPerformWorkspaceViewerActions: computed,
canPerformWorkspaceGuestActions: computed,
canPerformProjectAdminActions: computed,
canPerformWorkspaceCreateActions: computed,
canPerformProjectMemberActions: computed,
canPerformProjectViewerActions: computed,
canPerformProjectGuestActions: computed,
canPerformAnyCreateAction: computed,
projectsWithCreatePermissions: computed,
});
@ -273,15 +289,40 @@ export class UserStore implements IUserStore {
}
/**
* @description tells if user has project create actions permissions
* @description returns true if user has workspace admin actions permissions
* @returns {boolean}
*/
get canPerformProjectCreateActions() {
return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.MEMBER;
get canPerformWorkspaceAdminActions() {
return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
}
/**
* @description tells if user has project admin actions permissions
* @description returns true if user has workspace member actions permissions
* @returns {boolean}
*/
get canPerformWorkspaceMemberActions() {
return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
}
/**
* @description returns true if user has workspace viewer actions permissions
* @returns {boolean}
*/
get canPerformWorkspaceViewerActions() {
return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.VIEWER;
}
/**
* @description returns true if user has workspace guest actions permissions
* @returns {boolean}
*/
get canPerformWorkspaceGuestActions() {
return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.GUEST;
}
/**
* @description returns true if user has project admin actions permissions
* @returns {boolean}
*/
get canPerformProjectAdminActions() {
@ -289,10 +330,27 @@ export class UserStore implements IUserStore {
}
/**
* @description tells if user has workspace create actions permissions
* @description returns true if user has project member actions permissions
* @returns {boolean}
*/
get canPerformWorkspaceCreateActions() {
return !!this.membership.currentWorkspaceRole && this.membership.currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
get canPerformProjectMemberActions() {
return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.MEMBER;
}
/**
* @description returns true if user has project viewer actions permissions
* @returns {boolean}
*/
get canPerformProjectViewerActions() {
return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.VIEWER;
}
/**
* @description returns true if user has project guest actions permissions
* @returns {boolean}
*/
get canPerformProjectGuestActions() {
return !!this.membership.currentProjectRole && this.membership.currentProjectRole >= EUserProjectRoles.GUEST;
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB