fix: merge conflicts from preview

This commit is contained in:
sriram veeraghanta 2024-08-16 17:55:08 +05:30
commit 3729011cb0
283 changed files with 4895 additions and 5157 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

@ -26,7 +26,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">
@ -72,14 +72,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
@ -14,7 +15,7 @@ import { useUser } from "@/hooks/store";
// plane web constants
import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project";
export const ProjectSettingsSidebar = () => {
export const ProjectSettingsSidebar = observer(() => {
const { workspaceSlug, projectId } = useParams();
const pathname = usePathname();
// mobx store
@ -62,4 +63,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

@ -0,0 +1,16 @@
"use client";
import { ReactNode } from "react";
// components
import { AppHeader, ContentWrapper } from "@/components/core";
// local components
import { ProjectsListHeader } from "@/plane-web/components/projects/header";
import { ProjectsListMobileHeader } from "@/plane-web/components/projects/mobile-header";
export default function ProjectListLayout({ children }: { children: ReactNode }) {
return (
<>
<AppHeader header={<ProjectsListHeader />} mobileHeader={<ProjectsListMobileHeader />} />
<ContentWrapper>{children}</ContentWrapper>
</>
);
}

View file

@ -0,0 +1,4 @@
import { ProjectPageRoot } from "@/plane-web/components/projects/page";
const ProjectsPage = () => <ProjectPageRoot />;
export default ProjectsPage;

View file

@ -1,191 +0,0 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Search, Briefcase, X, ListFilter } from "lucide-react";
// types
import { TProjectFilters } from "@plane/types";
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
import { FiltersDropdown } from "@/components/issues";
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
import { cn } from "@/helpers/common.helper";
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useCommandPalette, useEventTracker, useMember, useProjectFilter, useUser } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
export const ProjectsListHeader = observer(() => {
// router
const { workspaceSlug } = useParams();
// states
const [isSearchOpen, setIsSearchOpen] = useState(false);
// refs
const inputRef = useRef<HTMLInputElement>(null);
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const {
membership: { currentWorkspaceRole },
} = useUser();
const {
currentWorkspaceDisplayFilters: displayFilters,
currentWorkspaceFilters: filters,
updateFilters,
updateDisplayFilters,
searchQuery,
updateSearchQuery,
} = useProjectFilter();
const {
workspace: { workspaceMemberIds },
} = useMember();
// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
});
// auth
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const handleFilters = useCallback(
(key: keyof TProjectFilters, value: string | string[]) => {
if (!workspaceSlug) return;
let newValues = filters?.[key] ?? [];
if (Array.isArray(value)) {
if (key === "created_at" && newValues.find((v) => v.includes("custom"))) newValues = [];
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
} else {
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else {
if (key === "created_at") newValues = [value];
else newValues.push(value);
}
}
updateFilters(workspaceSlug.toString(), { [key]: newValues });
},
[filters, updateFilters, workspaceSlug]
);
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
else setIsSearchOpen(false);
}
};
useEffect(() => {
if (searchQuery.trim() !== "") setIsSearchOpen(true);
}, [searchQuery]);
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
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">
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Projects" icon={<Briefcase className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
</div>
</div>
<div className="w-full flex items-center justify-end gap-3">
<div className="flex items-center">
{!isSearchOpen && (
<button
type="button"
className="-mr-1 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-30 md:w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
placeholder="Search"
value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
updateSearchQuery("");
setIsSearchOpen(false);
}}
>
<X className="h-3 w-3" />
</button>
)}
</div>
</div>
<div className="hidden md:flex gap-3">
<ProjectOrderByDropdown
value={displayFilters?.order_by}
onChange={(val) => {
if (!workspaceSlug || val === displayFilters?.order_by) return;
updateDisplayFilters(workspaceSlug.toString(), {
order_by: val,
});
}}
/>
<FiltersDropdown
icon={<ListFilter className="h-3 w-3" />}
title="Filters"
placement="bottom-end"
isFiltersApplied={isFiltersApplied}
>
<ProjectFiltersSelection
displayFilters={displayFilters ?? {}}
filters={filters ?? {}}
handleFiltersUpdate={handleFilters}
handleDisplayFiltersUpdate={(val) => {
if (!workspaceSlug) return;
updateDisplayFilters(workspaceSlug.toString(), val);
}}
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
</div>
{isAuthorizedUser && (
<Button
size="sm"
onClick={() => {
setTrackElement("Projects page");
toggleCreateProjectModal(true);
}}
className="items-center gap-1"
>
<span className="hidden sm:inline-block">Add</span> Project
</Button>
)}
</div>
</div>
);
});

View file

@ -4,9 +4,8 @@ import { ReactNode } from "react";
// components
import { AppHeader, ContentWrapper } from "@/components/core";
// local components
import { ProjectsListHeader } from "./header";
import { ProjectsListMobileHeader } from "./mobile-header";
import { ProjectsListHeader } from "@/plane-web/components/projects/header";
import { ProjectsListMobileHeader } from "@/plane-web/components/projects/mobile-header";
export default function ProjectListLayout({ children }: { children: ReactNode }) {
return (
<>

View file

@ -1,91 +0,0 @@
import { useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { ChevronDown, ListFilter } from "lucide-react";
// types
import { TProjectFilters } from "@plane/types";
// hooks
import { FiltersDropdown } from "@/components/issues/issue-layouts";
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project/dropdowns";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useMember, useProjectFilter } from "@/hooks/store";
export const ProjectsListMobileHeader = observer(() => {
// router
const { workspaceSlug } = useParams();
const {
currentWorkspaceDisplayFilters: displayFilters,
currentWorkspaceFilters: filters,
updateDisplayFilters,
updateFilters,
} = useProjectFilter();
const {
workspace: { workspaceMemberIds },
} = useMember();
const handleFilters = useCallback(
(key: keyof TProjectFilters, value: string | string[]) => {
if (!workspaceSlug) return;
const newValues = filters?.[key] ?? [];
if (Array.isArray(value))
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
else {
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(workspaceSlug.toString(), { [key]: newValues });
},
[filters, updateFilters, workspaceSlug]
);
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
return (
<div className="flex py-2 border-b border-custom-border-200 md:hidden bg-custom-background-100 w-full">
<ProjectOrderByDropdown
value={displayFilters?.order_by}
onChange={(val) => {
if (!workspaceSlug || val === displayFilters?.order_by) return;
updateDisplayFilters(workspaceSlug.toString(), {
order_by: val,
});
}}
isMobile
/>
<div className="border-l border-custom-border-200 flex justify-around w-full">
<FiltersDropdown
icon={<ListFilter className="h-3 w-3" />}
title="Filters"
placement="bottom-end"
menuButton={
<div className="flex text-sm items-center gap-2 neutral-primary text-custom-text-200">
<ListFilter className="h-3 w-3" />
Filters
<ChevronDown className="h-3 w-3" strokeWidth={2} />
</div>
}
isFiltersApplied={isFiltersApplied}
>
<ProjectFiltersSelection
displayFilters={displayFilters ?? {}}
filters={filters ?? {}}
handleFiltersUpdate={handleFilters}
handleDisplayFiltersUpdate={(val) => {
if (!workspaceSlug) return;
updateDisplayFilters(workspaceSlug.toString(), val);
}}
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
</div>
</div>
);
});

View file

@ -1,84 +1,4 @@
"use client";
import { useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// types
import { TProjectAppliedDisplayFilterKeys, TProjectFilters } from "@plane/types";
// components
import { PageHead } from "@/components/core";
import { ProjectAppliedFiltersList, ProjectCardList } from "@/components/project";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useProject, useProjectFilter, useWorkspace } from "@/hooks/store";
const ProjectsPage = observer(() => {
// store
const { workspaceSlug } = useParams();
const { currentWorkspace } = useWorkspace();
const { totalProjectIds, filteredProjectIds } = useProject();
const {
currentWorkspaceFilters,
currentWorkspaceAppliedDisplayFilters,
clearAllFilters,
clearAllAppliedDisplayFilters,
updateFilters,
updateDisplayFilters,
} = useProjectFilter();
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Projects` : undefined;
const handleRemoveFilter = useCallback(
(key: keyof TProjectFilters, value: string | null) => {
if (!workspaceSlug) return;
let newValues = currentWorkspaceFilters?.[key] ?? [];
if (!value) newValues = [];
else newValues = newValues.filter((val) => val !== value);
updateFilters(workspaceSlug.toString(), { [key]: newValues });
},
[currentWorkspaceFilters, updateFilters, workspaceSlug]
);
const handleRemoveDisplayFilter = useCallback(
(key: TProjectAppliedDisplayFilterKeys) => {
if (!workspaceSlug) return;
updateDisplayFilters(workspaceSlug.toString(), { [key]: false });
},
[updateDisplayFilters, workspaceSlug]
);
const handleClearAllFilters = useCallback(() => {
if (!workspaceSlug) return;
clearAllFilters(workspaceSlug.toString());
clearAllAppliedDisplayFilters(workspaceSlug.toString());
}, [clearAllFilters, clearAllAppliedDisplayFilters, workspaceSlug]);
return (
<>
<PageHead title={pageTitle} />
<div className="flex h-full w-full flex-col">
{(calculateTotalFilters(currentWorkspaceFilters ?? {}) !== 0 ||
currentWorkspaceAppliedDisplayFilters?.length !== 0) && (
<div className="border-b border-custom-border-200 px-5 py-3">
<ProjectAppliedFiltersList
appliedFilters={currentWorkspaceFilters ?? {}}
appliedDisplayFilters={currentWorkspaceAppliedDisplayFilters ?? []}
handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter}
handleRemoveDisplayFilter={handleRemoveDisplayFilter}
filteredProjects={filteredProjectIds?.length ?? 0}
totalProjects={totalProjectIds?.length ?? 0}
alwaysAllowEditing
/>
</div>
)}
<ProjectCardList />
</div>
</>
);
});
import { ProjectPageRoot } from "@/plane-web/components/projects/page";
const ProjectsPage = () => <ProjectPageRoot />;
export default ProjectsPage;

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
@ -54,10 +55,14 @@ export const AppSidebar: FC<IAppSidebar> = observer(() => {
<div
ref={ref}
className={cn("size-full flex flex-col flex-1 pt-4 pb-0", {
"p-2": sidebarCollapsed,
"p-2 pt-4": sidebarCollapsed,
})}
>
<div className="px-4">
<div
className={cn("px-2", {
"px-4": !sidebarCollapsed,
})}
>
<SidebarDropdown />
<div className="flex-shrink-0 h-4" />
<SidebarAppSwitcher />
@ -69,8 +74,8 @@ export const AppSidebar: FC<IAppSidebar> = observer(() => {
})}
/>
<div
className={cn("overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto px-4", {
"vertical-scrollbar": !sidebarCollapsed,
className={cn("overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto px-2", {
"vertical-scrollbar px-4": !sidebarCollapsed,
})}
>
<SidebarUserMenu />
@ -81,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

@ -154,6 +154,7 @@ export default function ForgotPasswordPage() {
hasError={Boolean(errors.email)}
placeholder="name@company.com"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
autoComplete="on"
disabled={resendTimerCode > 0}
/>
)}

View file

@ -153,6 +153,7 @@ export default function ResetPasswordPage() {
//hasError={Boolean(errors.email)}
placeholder="name@company.com"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed"
autoComplete="on"
disabled
/>
</div>
@ -173,6 +174,7 @@ export default function ResetPasswordPage() {
minLength={8}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="on"
autoFocus
/>
{showPassword.password ? (

View file

@ -147,6 +147,7 @@ const SetPasswordPage = observer(() => {
//hasError={Boolean(errors.email)}
placeholder="name@company.com"
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed"
autoComplete="on"
disabled
/>
</div>
@ -167,6 +168,7 @@ const SetPasswordPage = observer(() => {
minLength={8}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="on"
autoFocus
/>
{showPassword.password ? (

View file

@ -245,6 +245,7 @@ const ProfileSettingsPage = observer(() => {
placeholder="Enter your first name"
className={`w-full rounded-md ${errors.first_name ? "border-red-500" : ""}`}
maxLength={24}
autoComplete="on"
/>
)}
/>
@ -269,6 +270,7 @@ const ProfileSettingsPage = observer(() => {
placeholder="Enter your last name"
className="w-full rounded-md"
maxLength={24}
autoComplete="on"
/>
)}
/>
@ -296,6 +298,7 @@ const ProfileSettingsPage = observer(() => {
className={`w-full cursor-not-allowed rounded-md !bg-custom-background-80 ${
errors.email ? "border-red-500" : ""
}`}
autoComplete="on"
disabled
/>
)}