diff --git a/apiserver/plane/app/views/workspace/invite.py b/apiserver/plane/app/views/workspace/invite.py index 807c060ad2..0b6f03e8af 100644 --- a/apiserver/plane/app/views/workspace/invite.py +++ b/apiserver/plane/app/views/workspace/invite.py @@ -1,36 +1,39 @@ # Python imports -import jwt from datetime import datetime +import jwt + # Django imports from django.conf import settings -from django.utils import timezone -from django.db.models import Count from django.core.exceptions import ValidationError from django.core.validators import validate_email +from django.db.models import Count +from django.utils import timezone # Third party modules from rest_framework import status -from rest_framework.response import Response from rest_framework.permissions import AllowAny +from rest_framework.response import Response # Module imports +from plane.app.permissions import WorkSpaceAdminPermission from plane.app.serializers import ( - WorkSpaceMemberSerializer, WorkSpaceMemberInviteSerializer, + WorkSpaceMemberSerializer, ) from plane.app.views.base import BaseAPIView -from .. import BaseViewSet +from plane.bgtasks.event_tracking_task import workspace_invite_event +from plane.bgtasks.workspace_invitation_task import workspace_invitation from plane.db.models import ( User, Workspace, - WorkspaceMemberInvite, WorkspaceMember, + WorkspaceMemberInvite, ) -from plane.app.permissions import WorkSpaceAdminPermission -from plane.bgtasks.workspace_invitation_task import workspace_invitation -from plane.bgtasks.event_tracking_task import workspace_invite_event -from plane.utils.cache import invalidate_cache +from plane.utils.cache import invalidate_cache, invalidate_cache_directly + +from .. import BaseViewSet + class WorkspaceInvitationsViewset(BaseViewSet): """Endpoint for creating, listing and deleting workspaces""" @@ -265,9 +268,6 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet): @invalidate_cache(path="/api/workspaces/", user=False) @invalidate_cache(path="/api/users/me/workspaces/") - @invalidate_cache( - path="/api/workspaces/:slug/members/", url_params=True, user=False - ) def create(self, request): invitations = request.data.get("invitations", []) workspace_invitations = WorkspaceMemberInvite.objects.filter( @@ -276,6 +276,12 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet): # If the user is already a member of workspace and was deactivated then activate the user for invitation in workspace_invitations: + invalidate_cache_directly( + path=f"/api/workspaces/{invitation.workspace.slug}/members/", + user=False, + request=request, + multiple=True, + ) # Update the WorkspaceMember for this specific invitation WorkspaceMember.objects.filter( workspace_id=invitation.workspace_id, member=request.user diff --git a/apiserver/plane/app/views/workspace/member.py b/apiserver/plane/app/views/workspace/member.py index 5afe371442..6ea2b3f208 100644 --- a/apiserver/plane/app/views/workspace/member.py +++ b/apiserver/plane/app/views/workspace/member.py @@ -102,7 +102,10 @@ class WorkSpaceMemberViewSet(BaseViewSet): return Response(serializer.data, status=status.HTTP_200_OK) @invalidate_cache( - path="/api/workspaces/:slug/members/", url_params=True, user=False + path="/api/workspaces/:slug/members/", + url_params=True, + user=False, + multiple=True, ) def partial_update(self, request, slug, pk): workspace_member = WorkspaceMember.objects.get( @@ -147,9 +150,15 @@ class WorkSpaceMemberViewSet(BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @invalidate_cache( - path="/api/workspaces/:slug/members/", url_params=True, user=False + path="/api/workspaces/:slug/members/", + url_params=True, + user=False, + multiple=True, ) @invalidate_cache(path="/api/users/me/settings/") + @invalidate_cache( + path="/api/users/me/workspaces/", user=False, multiple=True + ) def destroy(self, request, slug, pk): # Check the user role who is deleting the user workspace_member = WorkspaceMember.objects.get( @@ -215,9 +224,15 @@ class WorkSpaceMemberViewSet(BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) @invalidate_cache( - path="/api/workspaces/:slug/members/", url_params=True, user=False + path="/api/workspaces/:slug/members/", + url_params=True, + user=False, + multiple=True, ) @invalidate_cache(path="/api/users/me/settings/") + @invalidate_cache( + path="api/users/me/workspaces/", user=False, multiple=True + ) def leave(self, request, slug): workspace_member = WorkspaceMember.objects.get( workspace__slug=slug, diff --git a/apiserver/plane/utils/cache.py b/apiserver/plane/utils/cache.py index aece1d644f..0710511296 100644 --- a/apiserver/plane/utils/cache.py +++ b/apiserver/plane/utils/cache.py @@ -33,12 +33,12 @@ def cache_response(timeout=60 * 60, path=None, user=True): custom_path = path if path is not None else request.get_full_path() key = generate_cache_key(custom_path, auth_header) cached_result = cache.get(key) + if cached_result is not None: return Response( cached_result["data"], status=cached_result["status"] ) response = view_func(instance, request, *args, **kwargs) - if response.status_code == 200 and not settings.DEBUG: cache.set( key, @@ -53,34 +53,42 @@ def cache_response(timeout=60 * 60, path=None, user=True): return decorator -def invalidate_cache(path=None, url_params=False, user=True): - """invalidate cache per user""" +def invalidate_cache_directly( + path=None, url_params=False, user=True, request=None, multiple=False +): + if url_params and path: + path_with_values = path + # Assuming `kwargs` could be passed directly if needed, otherwise, skip this part + for key, value in request.resolver_match.kwargs.items(): + path_with_values = path_with_values.replace(f":{key}", str(value)) + custom_path = path_with_values + else: + custom_path = path if path is not None else request.get_full_path() + auth_header = ( + None + if request.user.is_anonymous + else str(request.user.id) if user else None + ) + key = generate_cache_key(custom_path, auth_header) + if multiple: + cache.delete_many(keys=cache.keys(f"*{key}*")) + else: + cache.delete(key) + + +def invalidate_cache(path=None, url_params=False, user=True, multiple=False): def decorator(view_func): @wraps(view_func) def _wrapped_view(instance, request, *args, **kwargs): - # Invalidate cache before executing the view function - if url_params: - path_with_values = path - for key, value in kwargs.items(): - path_with_values = path_with_values.replace( - f":{key}", str(value) - ) - - custom_path = path_with_values - else: - custom_path = ( - path if path is not None else request.get_full_path() - ) - - auth_header = ( - None - if request.user.is_anonymous - else str(request.user.id) if user else None + # invalidate the cache + invalidate_cache_directly( + path=path, + url_params=url_params, + user=user, + request=request, + multiple=multiple, ) - key = generate_cache_key(custom_path, auth_header) - cache.delete(key) - # Execute the view function return view_func(instance, request, *args, **kwargs) return _wrapped_view diff --git a/packages/editor/core/src/ui/extensions/table/table/table-view.tsx b/packages/editor/core/src/ui/extensions/table/table/table-view.tsx index 2941179c7c..75b6bcb127 100644 --- a/packages/editor/core/src/ui/extensions/table/table/table-view.tsx +++ b/packages/editor/core/src/ui/extensions/table/table/table-view.tsx @@ -189,7 +189,7 @@ function createToolbox({ tippyOptions, onSelectColor, onClickItem, - colors, + colors = {}, }: { triggerButton: Element | null; items: ToolboxItem[]; diff --git a/packages/ui/src/icons/priority-icon.tsx b/packages/ui/src/icons/priority-icon.tsx index 0b98b3e6b6..031b769f18 100644 --- a/packages/ui/src/icons/priority-icon.tsx +++ b/packages/ui/src/icons/priority-icon.tsx @@ -16,7 +16,7 @@ export const PriorityIcon: React.FC = (props) => { const { priority, className = "", containerClassName = "", size = 14, withContainer = false } = props; const priorityClasses = { - urgent: "bg-red-500 text-red-500 border-red-500", + urgent: "bg-red-600 text-red-500 border-red-600", high: "bg-orange-500/20 text-orange-500 border-orange-500", medium: "bg-yellow-500/20 text-yellow-500 border-yellow-500", low: "bg-custom-primary-100/20 text-custom-primary-100 border-custom-primary-100", @@ -40,7 +40,7 @@ export const PriorityIcon: React.FC = (props) => { {withContainer ? (
= (props) => { str.replace(/_/g, " "); export const AppliedFiltersList: React.FC = (props) => { - const { appliedFilters, handleRemoveAllFilters, handleRemoveFilter, states } = props; + const { appliedFilters = {}, handleRemoveAllFilters, handleRemoveFilter, states } = props; return (
diff --git a/web/components/dropdowns/priority.tsx b/web/components/dropdowns/priority.tsx index 49dd5b2c50..d16e31f269 100644 --- a/web/components/dropdowns/priority.tsx +++ b/web/components/dropdowns/priority.tsx @@ -79,7 +79,7 @@ const BorderButton = (props: ButtonProps) => { // compact the icons if text is hidden "px-0.5": hideText, // highlight the whole button if text is hidden and priority is urgent - "bg-red-500 border-red-500": priority === "urgent" && hideText && highlightUrgent, + "bg-red-600 border-red-600": priority === "urgent" && hideText && highlightUrgent, }, className )} @@ -88,7 +88,7 @@ const BorderButton = (props: ButtonProps) => {
{ // compact the icons if text is hidden "px-0.5": hideText, // highlight the whole button if text is hidden and priority is urgent - "bg-red-500 border-red-500": priority === "urgent" && hideText && highlightUrgent, + "bg-red-600 border-red-600": priority === "urgent" && hideText && highlightUrgent, }, className )} @@ -164,7 +164,7 @@ const BackgroundButton = (props: ButtonProps) => {
{ // compact the icons if text is hidden "px-0.5": hideText, // highlight the whole button if text is hidden and priority is urgent - "bg-red-500 border-red-500": priority === "urgent" && hideText && highlightUrgent, + "bg-red-600 border-red-600": priority === "urgent" && hideText && highlightUrgent, "bg-custom-background-80": isActive, }, className @@ -242,7 +242,7 @@ const TransparentButton = (props: ButtonProps) => {
) => void; handleDisplayPropertiesUpdate: (updatedDisplayProperties: Partial) => void; @@ -80,8 +80,8 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { {/* sub-group by */} {isDisplayFilterEnabled("sub_group_by") && - displayFilters.group_by !== null && - displayFilters.layout === "kanban" && ( + displayFilters?.group_by !== null && + displayFilters?.layout === "kanban" && (
= observer((props) => { {isDisplayFilterEnabled("order_by") && (
handleDisplayFiltersUpdate({ order_by: val, @@ -115,7 +115,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { {isDisplayFilterEnabled("type") && (
handleDisplayFiltersUpdate({ type: val, @@ -130,8 +130,8 @@ export const DisplayFiltersSelection: React.FC = observer((props) => {
handleDisplayFiltersUpdate({ diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx index f05452f218..f103e55a60 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx @@ -8,7 +8,7 @@ import { ISSUE_GROUP_BY_OPTIONS } from "@/constants/issue"; // constants type Props = { - displayFilters: IIssueDisplayFilterOptions; + displayFilters: IIssueDisplayFilterOptions | undefined; groupByOptions: TIssueGroupByOptions[]; handleUpdate: (val: TIssueGroupByOptions) => void; ignoreGroupedFilters: Partial[]; @@ -19,8 +19,8 @@ export const FilterGroupBy: React.FC = observer((props) => { const [previewEnabled, setPreviewEnabled] = useState(true); - const selectedGroupBy = displayFilters.group_by ?? null; - const selectedSubGroupBy = displayFilters.sub_group_by ?? null; + const selectedGroupBy = displayFilters?.group_by ?? null; + const selectedSubGroupBy = displayFilters?.sub_group_by ?? null; return ( <> @@ -32,7 +32,11 @@ export const FilterGroupBy: React.FC = observer((props) => { {previewEnabled && (
{ISSUE_GROUP_BY_OPTIONS.filter((option) => groupByOptions.includes(option.key)).map((groupBy) => { - if (displayFilters.layout === "kanban" && selectedSubGroupBy !== null && groupBy.key === selectedSubGroupBy) + if ( + displayFilters?.layout === "kanban" && + selectedSubGroupBy !== null && + groupBy.key === selectedSubGroupBy + ) return null; if (ignoreGroupedFilters.includes(groupBy?.key)) return null; diff --git a/web/components/modules/select/status.tsx b/web/components/modules/select/status.tsx index 7db3167f56..6173331473 100644 --- a/web/components/modules/select/status.tsx +++ b/web/components/modules/select/status.tsx @@ -24,7 +24,7 @@ export const ModuleStatusSelect: React.FC = ({ control, error, tabIndex } +
{value ? ( ) : ( diff --git a/web/components/project/applied-filters/root.tsx b/web/components/project/applied-filters/root.tsx index 7c4381989a..83eaf5defe 100644 --- a/web/components/project/applied-filters/root.tsx +++ b/web/components/project/applied-filters/root.tsx @@ -48,7 +48,7 @@ export const ProjectAppliedFiltersList: React.FC = (props) => {
{/* Applied filters */} - {Object.entries(appliedFilters).map(([key, value]) => { + {Object.entries(appliedFilters ?? {}).map(([key, value]) => { const filterKey = key as keyof TProjectFilters; if (!value) return; diff --git a/web/components/project/create-project-form.tsx b/web/components/project/create-project-form.tsx index 27a19d7f83..8fa1b390f6 100644 --- a/web/components/project/create-project-form.tsx +++ b/web/components/project/create-project-form.tsx @@ -197,16 +197,16 @@ export const CreateProjectForm: FC = observer((props) => { onChange={(val: any) => { let logoValue = {}; - if (val.type === "emoji") + if (val?.type === "emoji") logoValue = { value: convertHexEmojiToDecimal(val.value.unified), url: val.value.imageUrl, }; - else if (val.type === "icon") logoValue = val.value; + else if (val?.type === "icon") logoValue = val.value; onChange({ - in_use: val.type, - [val.type]: logoValue, + in_use: val?.type, + [val?.type]: logoValue, }); }} defaultIconColor={value.in_use && value.in_use === "icon" ? value.icon?.color : undefined} diff --git a/web/components/project/form.tsx b/web/components/project/form.tsx index 98a3e8d02a..f2a87a173d 100644 --- a/web/components/project/form.tsx +++ b/web/components/project/form.tsx @@ -157,16 +157,16 @@ export const ProjectDetailsForm: FC = (props) => { onChange={(val) => { let logoValue = {}; - if (val.type === "emoji") + if (val?.type === "emoji") logoValue = { value: convertHexEmojiToDecimal(val.value.unified), url: val.value.imageUrl, }; - else if (val.type === "icon") logoValue = val.value; + else if (val?.type === "icon") logoValue = val.value; onChange({ - in_use: val.type, - [val.type]: logoValue, + in_use: val?.type, + [val?.type]: logoValue, }); }} defaultIconColor={value?.in_use && value.in_use === "icon" ? value?.icon?.color : undefined} diff --git a/web/constants/empty-state.ts b/web/constants/empty-state.ts index 16d4dd0e4e..0602f0c233 100644 --- a/web/constants/empty-state.ts +++ b/web/constants/empty-state.ts @@ -93,6 +93,10 @@ export enum EmptyStateType { WORKSPACE_ACTIVE_CYCLES = "workspace-active-cycles", DISABLED_PROJECT_INBOX = "disabled-project-inbox", + DISABLED_PROJECT_CYCLE = "disabled-project-cycle", + DISABLED_PROJECT_MODULE = "disabled-project-module", + DISABLED_PROJECT_VIEW = "disabled-project-view", + DISABLED_PROJECT_PAGE = "disabled-project-page", INBOX_SIDEBAR_OPEN_TAB = "inbox-sidebar-open-tab", INBOX_SIDEBAR_CLOSED_TAB = "inbox-sidebar-closed-tab", @@ -642,6 +646,54 @@ const emptyStateDetails = { text: "Manage features", }, }, + [EmptyStateType.DISABLED_PROJECT_CYCLE]: { + key: EmptyStateType.DISABLED_PROJECT_CYCLE, + title: "Cycles is not enabled for this project.", + description: + "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team. Enable the cycles feature for your project to start using them.", + accessType: "project", + access: EUserProjectRoles.ADMIN, + path: "/empty-state/disabled-feature/cycles", + primaryButton: { + text: "Manage features", + }, + }, + [EmptyStateType.DISABLED_PROJECT_MODULE]: { + key: EmptyStateType.DISABLED_PROJECT_MODULE, + title: "Modules are not enabled for the project.", + description: + "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. Enable modules from project settings.", + accessType: "project", + access: EUserProjectRoles.ADMIN, + path: "/empty-state/disabled-feature/modules", + primaryButton: { + text: "Manage features", + }, + }, + [EmptyStateType.DISABLED_PROJECT_PAGE]: { + key: EmptyStateType.DISABLED_PROJECT_PAGE, + title: "Pages are not enabled for the project.", + description: + "Pages are thought spotting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. Enable the pages feature to start creating them in your project.", + accessType: "project", + access: EUserProjectRoles.ADMIN, + path: "/empty-state/disabled-feature/pages", + primaryButton: { + text: "Manage features", + }, + }, + [EmptyStateType.DISABLED_PROJECT_VIEW]: { + key: EmptyStateType.DISABLED_PROJECT_VIEW, + title: "Views is not enabled for this project.", + description: + "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best. Enable views in the project settings to start using them.", + accessType: "project", + access: EUserProjectRoles.ADMIN, + path: "/empty-state/disabled-feature/views", + primaryButton: { + text: "Manage features", + }, + }, [EmptyStateType.INBOX_SIDEBAR_OPEN_TAB]: { key: EmptyStateType.INBOX_SIDEBAR_OPEN_TAB, title: "No open issues", diff --git a/web/constants/fetch-keys.ts b/web/constants/fetch-keys.ts index 4e67b2a836..591a5240a7 100644 --- a/web/constants/fetch-keys.ts +++ b/web/constants/fetch-keys.ts @@ -59,9 +59,9 @@ const myIssuesParamsToKey = (params: any) => { let labelsKey = labels ? labels.split(",") : []; const startDateKey = start_date ?? ""; const targetDateKey = target_date ?? ""; - const type = params.type ? params.type.toUpperCase() : "NULL"; - const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL"; - const orderBy = params.order_by ? params.order_by.toUpperCase() : "NULL"; + const type = params?.type ? params.type.toUpperCase() : "NULL"; + const groupBy = params?.group_by ? params.group_by.toUpperCase() : "NULL"; + const orderBy = params?.order_by ? params.order_by.toUpperCase() : "NULL"; // sorting each keys in ascending order assigneesKey = assigneesKey.sort().join("_"); diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index 03233a9181..558c888834 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -152,6 +152,8 @@ export const getNumberCount = (number: number): string => { export const objToQueryParams = (obj: any) => { const params = new URLSearchParams(); + if (!obj) return params.toString(); + for (const [key, value] of Object.entries(obj)) { if (value !== undefined && value !== null) params.append(key, value as string); } diff --git a/web/hooks/use-reload-confirmation.tsx b/web/hooks/use-reload-confirmation.tsx index 8343ea78df..6cf6d3e88b 100644 --- a/web/hooks/use-reload-confirmation.tsx +++ b/web/hooks/use-reload-confirmation.tsx @@ -21,7 +21,6 @@ const useReloadConfirmations = (isActive = true) => { const leave = confirm("Are you sure you want to leave? Changes you made may not be saved."); if (!leave) { router.events.emit("routeChangeError"); - throw `Route change to "${url}" was aborted (this error can be safely ignored).`; } }, [isActive, showAlert, router.events] diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 5fd59d871a..fdf3cb6d76 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -35,7 +35,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { // store hooks const { setTrackElement } = useEventTracker(); const { currentProjectCycleIds, loader } = useCycle(); - const { getProjectById } = useProject(); + const { getProjectById, currentProjectDetails } = useProject(); // router const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; @@ -60,7 +60,18 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { updateFilters(projectId.toString(), { [key]: newValues }); }; - if (!workspaceSlug || !projectId) return null; + if (!workspaceSlug || !projectId) return <>; + + // No access to cycle + if (currentProjectDetails?.cycle_view === false) + return ( +
+ +
+ ); if (loader) return ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index 8f626bfdf3..22973347f6 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -5,11 +5,13 @@ import { TModuleFilters } from "@plane/types"; // layouts // components import { PageHead } from "@/components/core"; +import { EmptyState } from "@/components/empty-state"; import { ModulesListHeader } from "@/components/headers"; import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules"; // types // hooks import ModulesListMobileHeader from "@/components/modules/moduels-list-mobile-header"; +import { EmptyStateType } from "@/constants/empty-state"; import { calculateTotalFilters } from "@/helpers/filter.helper"; import { useModuleFilter, useProject } from "@/hooks/store"; import { AppLayout } from "@/layouts/app-layout"; @@ -17,9 +19,9 @@ import { NextPageWithLayout } from "@/lib/types"; const ProjectModulesPage: NextPageWithLayout = observer(() => { const router = useRouter(); - const { projectId } = router.query; + const { workspaceSlug, projectId } = router.query; // store - const { getProjectById } = useProject(); + const { getProjectById, currentProjectDetails } = useProject(); const { currentProjectFilters, clearAllFilters, updateFilters } = useModuleFilter(); // derived values const project = projectId ? getProjectById(projectId.toString()) : undefined; @@ -38,6 +40,19 @@ const ProjectModulesPage: NextPageWithLayout = observer(() => { [currentProjectFilters, projectId, updateFilters] ); + if (!workspaceSlug || !projectId) return <>; + + // No access to + if (currentProjectDetails?.module_view === false) + return ( +
+ +
+ ); + return ( <> diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index 1912565a79..e7e7a3e2bd 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -56,7 +56,7 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { commandPalette: { toggleCreatePageModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { getProjectById } = useProject(); + const { getProjectById, currentProjectDetails } = useProject(); const { fetchProjectPages, fetchArchivedProjectPages, loader, archivedPageLoader, projectPageIds, archivedPageIds } = useProjectPages(); // hooks @@ -75,6 +75,19 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { workspaceSlug && projectId ? () => fetchArchivedProjectPages(workspaceSlug.toString(), projectId.toString()) : null ); + if (!workspaceSlug || !projectId) return <>; + + // No access to + if (currentProjectDetails?.page_view === false) + return ( +
+ +
+ ); + const currentTabValue = (tab: string | null) => { switch (tab) { case "Recent": diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index 59c7aa2153..dbdc0f192e 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -3,8 +3,11 @@ import { observer } from "mobx-react"; import { useRouter } from "next/router"; // components import { PageHead } from "@/components/core"; +import { EmptyState } from "@/components/empty-state"; import { ProjectViewsHeader } from "@/components/headers"; import { ProjectViewsList } from "@/components/views"; +// constants +import { EmptyStateType } from "@/constants/empty-state"; // hooks import { useProject } from "@/hooks/store"; // layouts @@ -15,13 +18,26 @@ import { NextPageWithLayout } from "@/lib/types"; const ProjectViewsPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); - const { projectId } = router.query; + const { workspaceSlug, projectId } = router.query; // store - const { getProjectById } = useProject(); + const { getProjectById, currentProjectDetails } = useProject(); // derived values const project = projectId ? getProjectById(projectId.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Views` : undefined; + if (!workspaceSlug || !projectId) return <>; + + // No access to + if (currentProjectDetails?.issue_views_view === false) + return ( +
+ +
+ ); + return ( <> diff --git a/web/pages/installations/[provider]/index.tsx b/web/pages/installations/[provider]/index.tsx index c66d7d7c56..0ce6f9eca5 100644 --- a/web/pages/installations/[provider]/index.tsx +++ b/web/pages/installations/[provider]/index.tsx @@ -55,7 +55,7 @@ const AppPostInstallation: NextPageWithLayout = () => { window.close(); }) .catch((err) => { - throw err.response; + throw err?.response; }); } } diff --git a/web/public/empty-state/disabled-feature/cycles-dark.webp b/web/public/empty-state/disabled-feature/cycles-dark.webp new file mode 100644 index 0000000000..5b2234984d Binary files /dev/null and b/web/public/empty-state/disabled-feature/cycles-dark.webp differ diff --git a/web/public/empty-state/disabled-feature/cycles-light.webp b/web/public/empty-state/disabled-feature/cycles-light.webp new file mode 100644 index 0000000000..2e6e01e3b8 Binary files /dev/null and b/web/public/empty-state/disabled-feature/cycles-light.webp differ diff --git a/web/public/empty-state/disabled-feature/modules-dark.webp b/web/public/empty-state/disabled-feature/modules-dark.webp new file mode 100644 index 0000000000..8e198ed334 Binary files /dev/null and b/web/public/empty-state/disabled-feature/modules-dark.webp differ diff --git a/web/public/empty-state/disabled-feature/modules-light.webp b/web/public/empty-state/disabled-feature/modules-light.webp new file mode 100644 index 0000000000..d1db65cc04 Binary files /dev/null and b/web/public/empty-state/disabled-feature/modules-light.webp differ diff --git a/web/public/empty-state/disabled-feature/pages-dark.webp b/web/public/empty-state/disabled-feature/pages-dark.webp new file mode 100644 index 0000000000..19263a0cb3 Binary files /dev/null and b/web/public/empty-state/disabled-feature/pages-dark.webp differ diff --git a/web/public/empty-state/disabled-feature/pages-light.webp b/web/public/empty-state/disabled-feature/pages-light.webp new file mode 100644 index 0000000000..fd1c5b4d61 Binary files /dev/null and b/web/public/empty-state/disabled-feature/pages-light.webp differ diff --git a/web/public/empty-state/disabled-feature/views-dark.webp b/web/public/empty-state/disabled-feature/views-dark.webp new file mode 100644 index 0000000000..c82d967ba9 Binary files /dev/null and b/web/public/empty-state/disabled-feature/views-dark.webp differ diff --git a/web/public/empty-state/disabled-feature/views-light.webp b/web/public/empty-state/disabled-feature/views-light.webp new file mode 100644 index 0000000000..145bc8c243 Binary files /dev/null and b/web/public/empty-state/disabled-feature/views-light.webp differ