Merge branch 'sync/ce-ee' of github.com:makeplane/plane-ee into develop

This commit is contained in:
sriram veeraghanta
2024-04-09 13:48:43 +05:30
30 changed files with 227 additions and 85 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -189,7 +189,7 @@ function createToolbox({
tippyOptions,
onSelectColor,
onClickItem,
colors,
colors = {},
}: {
triggerButton: Element | null;
items: ToolboxItem[];

View File

@@ -16,7 +16,7 @@ export const PriorityIcon: React.FC<IPriorityIcon> = (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<IPriorityIcon> = (props) => {
{withContainer ? (
<div
className={cn(
"grid place-items-center border rounded p-0.5 flex-shrink-0",
"flex items-center justify-center border rounded p-0.5 flex-shrink-0",
priorityClasses[priority],
containerClassName
)}
@@ -63,8 +63,9 @@ export const PriorityIcon: React.FC<IPriorityIcon> = (props) => {
<Icon
size={size}
className={cn(
"flex-shrink-0",
{
"text-red-500": priority === "urgent",
"text-red-600": priority === "urgent",
"text-orange-500": priority === "high",
"text-yellow-500": priority === "medium",
"text-custom-primary-100": priority === "low",

View File

@@ -19,7 +19,7 @@ type Props = {
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
export const AppliedFiltersList: React.FC<Props> = (props) => {
const { appliedFilters, handleRemoveAllFilters, handleRemoveFilter, states } = props;
const { appliedFilters = {}, handleRemoveAllFilters, handleRemoveFilter, states } = props;
return (
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">

View File

@@ -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) => {
<div
className={cn({
// highlight just the icon if text is visible and priority is urgent
"bg-red-500 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
"bg-red-600 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
})}
>
<PriorityIcon
@@ -155,7 +155,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,
},
className
)}
@@ -164,7 +164,7 @@ const BackgroundButton = (props: ButtonProps) => {
<div
className={cn({
// highlight just the icon if text is visible and priority is urgent
"bg-red-500 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
"bg-red-600 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
})}
>
<PriorityIcon
@@ -232,7 +232,7 @@ const TransparentButton = (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) => {
<div
className={cn({
// highlight just the icon if text is visible and priority is urgent
"bg-red-500 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
"bg-red-600 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
})}
>
<PriorityIcon

View File

@@ -15,7 +15,7 @@ import {
import { ILayoutDisplayFiltersOptions } from "@/constants/issue";
type Props = {
displayFilters: IIssueDisplayFilterOptions;
displayFilters: IIssueDisplayFilterOptions | undefined;
displayProperties: IIssueDisplayProperties;
handleDisplayFiltersUpdate: (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => void;
handleDisplayPropertiesUpdate: (updatedDisplayProperties: Partial<IIssueDisplayProperties>) => void;
@@ -80,8 +80,8 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
{/* sub-group by */}
{isDisplayFilterEnabled("sub_group_by") &&
displayFilters.group_by !== null &&
displayFilters.layout === "kanban" && (
displayFilters?.group_by !== null &&
displayFilters?.layout === "kanban" && (
<div className="py-2">
<FilterSubGroupBy
displayFilters={displayFilters}
@@ -100,7 +100,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
{isDisplayFilterEnabled("order_by") && (
<div className="py-2">
<FilterOrderBy
selectedOrderBy={displayFilters.order_by}
selectedOrderBy={displayFilters?.order_by}
handleUpdate={(val) =>
handleDisplayFiltersUpdate({
order_by: val,
@@ -115,7 +115,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
{isDisplayFilterEnabled("type") && (
<div className="py-2">
<FilterIssueType
selectedIssueType={displayFilters.type}
selectedIssueType={displayFilters?.type}
handleUpdate={(val) =>
handleDisplayFiltersUpdate({
type: val,
@@ -130,8 +130,8 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
<div className="py-2">
<FilterExtraOptions
selectedExtraOptions={{
show_empty_groups: displayFilters.show_empty_groups ?? true,
sub_issue: displayFilters.sub_issue ?? true,
show_empty_groups: displayFilters?.show_empty_groups ?? true,
sub_issue: displayFilters?.sub_issue ?? true,
}}
handleUpdate={(key, val) =>
handleDisplayFiltersUpdate({

View File

@@ -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<TIssueGroupByOptions>[];
@@ -19,8 +19,8 @@ export const FilterGroupBy: React.FC<Props> = 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<Props> = observer((props) => {
{previewEnabled && (
<div>
{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;

View File

@@ -24,7 +24,7 @@ export const ModuleStatusSelect: React.FC<Props> = ({ control, error, tabIndex }
<CustomSelect
value={value}
label={
<div className={`flex items-center justify-center gap-2 text-xs ${error ? "text-red-500" : ""}`}>
<div className={`flex items-center justify-center gap-2 text-xs py-0.5 ${error ? "text-red-500" : ""}`}>
{value ? (
<ModuleStatusIcon status={value} />
) : (

View File

@@ -48,7 +48,7 @@ export const ProjectAppliedFiltersList: React.FC<Props> = (props) => {
<div className="flex items-start justify-between gap-1.5">
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
{/* Applied filters */}
{Object.entries(appliedFilters).map(([key, value]) => {
{Object.entries(appliedFilters ?? {}).map(([key, value]) => {
const filterKey = key as keyof TProjectFilters;
if (!value) return;

View File

@@ -197,16 +197,16 @@ export const CreateProjectForm: FC<Props> = 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}

View File

@@ -157,16 +157,16 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (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}

View File

@@ -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 projects 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 everyones 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",

View File

@@ -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("_");

View File

@@ -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);
}

View File

@@ -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]

View File

@@ -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 (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_CYCLE}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
/>
</div>
);
if (loader)
return (

View File

@@ -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 (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_MODULE}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
/>
</div>
);
return (
<>
<PageHead title={pageTitle} />

View File

@@ -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 (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_PAGE}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
/>
</div>
);
const currentTabValue = (tab: string | null) => {
switch (tab) {
case "Recent":

View File

@@ -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 (
<div className="flex items-center justify-center h-full w-full">
<EmptyState
type={EmptyStateType.DISABLED_PROJECT_VIEW}
primaryButtonLink={`/${workspaceSlug}/projects/${projectId}/settings/features`}
/>
</div>
);
return (
<>
<PageHead title={pageTitle} />

View File

@@ -55,7 +55,7 @@ const AppPostInstallation: NextPageWithLayout = () => {
window.close();
})
.catch((err) => {
throw err.response;
throw err?.response;
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB