[WEB-3998] feat: settings page revamp (#6959)
* chore: return workspace name and logo in profile settings api * chore: remove unwanted fields * fix: backend * feat: workspace settings * feat: workspce settings + layouting * feat: profile + workspace settings ui * chore: project settings + refactoring * routes * fix: handled no project * fix: css + build * feat: profile settings internal screens upgrade * fix: workspace settings internal screens * fix: external scrolling allowed * fix: css * fix: css * fix: css * fix: preferences settings * fix: css * fix: mobile interface * fix: profile redirections * fix: dark theme * fix: css * fix: css * feat: scroll * fix: refactor * fix: bug fixes * fix: refactor * fix: css * fix: routes * fix: first day of the week * fix: scrolling * fix: refactoring * fix: project -> projects * fix: refactoring * fix: refactor * fix: no authorized view consistency * fix: folder structure * fix: revert * fix: handled redirections * fix: scroll * fix: deleted old routes * fix: empty states * fix: headings * fix: settings description * fix: build --------- Co-authored-by: gakshita <akshitagoyal1516@gmail.com> Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com>
This commit is contained in:
parent
445c819fbd
commit
41c2aefad4
112 changed files with 2789 additions and 975 deletions
|
|
@ -69,7 +69,7 @@ const ProjectCyclesPage = observer(() => {
|
|||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.cycle.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
},
|
||||
disabled: !hasAdminLevelPermission,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const ProjectInboxPage = observer(() => {
|
|||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.inbox.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ const ProjectModulesPage = observer(() => {
|
|||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.module.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const ProjectPagesPage = observer(() => {
|
|||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.page.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
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";
|
||||
// hooks
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
const AutomationSettingsPage = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
const { currentProjectDetails: projectDetails, updateProject } = useProject();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// derived values
|
||||
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||
|
||||
const handleChange = async (formData: Partial<IProject>) => {
|
||||
if (!workspaceSlug || !projectId || !projectDetails) return;
|
||||
|
||||
await updateProject(workspaceSlug.toString(), projectId.toString(), formData).catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// derived values
|
||||
const pageTitle = projectDetails?.name ? `${projectDetails?.name} - Automations` : undefined;
|
||||
|
||||
if (workspaceUserInfo && !canPerformProjectAdminActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className={`w-full overflow-y-auto ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
|
||||
<div className="flex flex-col items-start border-b border-custom-border-100 pb-3.5">
|
||||
<h3 className="text-xl font-medium leading-normal">{t("project_settings.automations.label")}</h3>
|
||||
</div>
|
||||
<AutoArchiveAutomation handleChange={handleChange} />
|
||||
<AutoCloseAutomation handleChange={handleChange} />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default AutomationSettingsPage;
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EstimateRoot } from "@/components/estimates";
|
||||
// hooks
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
const EstimatesSettingsPage = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
|
||||
// derived values
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Estimates` : undefined;
|
||||
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (workspaceUserInfo && !canPerformProjectAdminActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div
|
||||
className={`w-full overflow-y-auto ${canPerformProjectAdminActions ? "" : "pointer-events-none opacity-60"}`}
|
||||
>
|
||||
<EstimateRoot
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
isAdmin={canPerformProjectAdminActions}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default EstimatesSettingsPage;
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectFeaturesList } from "@/components/project";
|
||||
// hooks
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
const FeaturesSettingsPage = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
|
||||
const { currentProjectDetails } = useProject();
|
||||
// derived values
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined;
|
||||
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||
|
||||
if (!workspaceSlug || !projectId) return null;
|
||||
|
||||
if (workspaceUserInfo && !canPerformProjectAdminActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className={`w-full overflow-y-auto ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
|
||||
<ProjectFeaturesList
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
isAdmin={canPerformProjectAdminActions}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default FeaturesSettingsPage;
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
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 { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectSettingsLabelList } from "@/components/labels";
|
||||
// hooks
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
const LabelsSettingsPage = observer(() => {
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Labels` : undefined;
|
||||
|
||||
const scrollableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// derived values
|
||||
const canPerformProjectMemberActions = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
|
||||
// Enable Auto Scroll for Labels list
|
||||
useEffect(() => {
|
||||
const element = scrollableContainerRef.current;
|
||||
|
||||
if (!element) return;
|
||||
|
||||
return combine(
|
||||
autoScrollForElements({
|
||||
element,
|
||||
})
|
||||
);
|
||||
}, [scrollableContainerRef?.current]);
|
||||
|
||||
if (workspaceUserInfo && !canPerformProjectMemberActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div ref={scrollableContainerRef} className="h-full w-full gap-10 overflow-y-auto">
|
||||
<ProjectSettingsLabelList />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default LabelsSettingsPage;
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC, ReactNode } from "react";
|
||||
// components
|
||||
import { AppHeader } from "@/components/core";
|
||||
// local components
|
||||
import { ProjectSettingHeader } from "../header";
|
||||
import { ProjectSettingsSidebar } from "./sidebar";
|
||||
|
||||
export interface IProjectSettingLayout {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const ProjectSettingLayout: FC<IProjectSettingLayout> = (props) => {
|
||||
const { children } = props;
|
||||
return (
|
||||
<>
|
||||
<AppHeader header={<ProjectSettingHeader />} />
|
||||
<div className="inset-y-0 flex flex-row vertical-scrollbar scrollbar-lg h-full w-full overflow-y-auto">
|
||||
<div className="px-page-x !pr-0 py-page-y flex-shrink-0 overflow-y-hidden sm:hidden hidden md:block lg:block">
|
||||
<ProjectSettingsSidebar />
|
||||
</div>
|
||||
<div className="flex flex-col relative w-full overflow-hidden">
|
||||
<div className="h-full w-full overflow-x-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-page-x md:px-9 py-page-y">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectSettingLayout;
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectMemberList, ProjectSettingsMemberDefaults } from "@/components/project";
|
||||
// hooks
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
const MembersSettingsPage = observer(() => {
|
||||
// store
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined;
|
||||
const isProjectMemberOrAdmin = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const canPerformProjectMemberActions = isProjectMemberOrAdmin || isWorkspaceAdmin;
|
||||
|
||||
if (workspaceUserInfo && !canPerformProjectMemberActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className={`w-full overflow-y-auto`}>
|
||||
<ProjectSettingsMemberDefaults />
|
||||
<ProjectMemberList />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default MembersSettingsPage;
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { PageHead } from "@/components/core";
|
||||
import {
|
||||
ArchiveRestoreProjectModal,
|
||||
ArchiveProjectSelection,
|
||||
DeleteProjectModal,
|
||||
DeleteProjectSection,
|
||||
ProjectDetailsForm,
|
||||
ProjectDetailsFormLoader,
|
||||
} from "@/components/project";
|
||||
// hooks
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
const GeneralSettingsPage = observer(() => {
|
||||
// states
|
||||
const [selectProject, setSelectedProject] = useState<string | null>(null);
|
||||
const [archiveProject, setArchiveProject] = useState<boolean>(false);
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { currentProjectDetails, fetchProjectDetails } = useProject();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
// api call to fetch project details
|
||||
// TODO: removed this API if not necessary
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) : null
|
||||
);
|
||||
// derived values
|
||||
const isAdmin = allowPermissions(
|
||||
[EUserPermissions.ADMIN],
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString()
|
||||
);
|
||||
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
{currentProjectDetails && workspaceSlug && projectId && (
|
||||
<>
|
||||
<ArchiveRestoreProjectModal
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
isOpen={archiveProject}
|
||||
onClose={() => setArchiveProject(false)}
|
||||
archive
|
||||
/>
|
||||
<DeleteProjectModal
|
||||
project={currentProjectDetails}
|
||||
isOpen={Boolean(selectProject)}
|
||||
onClose={() => setSelectedProject(null)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={`w-full overflow-y-auto ${isAdmin ? "" : "opacity-60"}`}>
|
||||
{currentProjectDetails && workspaceSlug && projectId && !isLoading ? (
|
||||
<ProjectDetailsForm
|
||||
project={currentProjectDetails}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
) : (
|
||||
<ProjectDetailsFormLoader />
|
||||
)}
|
||||
|
||||
{isAdmin && currentProjectDetails && (
|
||||
<>
|
||||
<ArchiveProjectSelection
|
||||
projectDetails={currentProjectDetails}
|
||||
handleArchive={() => setArchiveProject(true)}
|
||||
/>
|
||||
<DeleteProjectSection
|
||||
projectDetails={currentProjectDetails}
|
||||
handleDelete={() => setSelectedProject(currentProjectDetails.id ?? null)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default GeneralSettingsPage;
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import range from "lodash/range";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
// plane web constants
|
||||
import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project";
|
||||
|
||||
export const ProjectSettingsSidebar = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const pathname = usePathname();
|
||||
// mobx store
|
||||
const { allowPermissions, projectUserInfo } = useUserPermissions();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// derived values
|
||||
const currentProjectRole = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]?.role;
|
||||
|
||||
if (!currentProjectRole) {
|
||||
return (
|
||||
<div className="flex w-[280px] flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-semibold text-custom-sidebar-text-400">SETTINGS</span>
|
||||
<Loader className="flex w-full flex-col gap-2">
|
||||
{range(8).map((index) => (
|
||||
<Loader.Item key={index} height="34px" />
|
||||
))}
|
||||
</Loader>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-[280px] flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-semibold text-custom-sidebar-text-400">SETTINGS</span>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
{PROJECT_SETTINGS_LINKS.map(
|
||||
(link) =>
|
||||
allowPermissions(
|
||||
link.access,
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString()
|
||||
) && (
|
||||
<Link key={link.key} href={`/${workspaceSlug}/projects/${projectId}${link.href}`}>
|
||||
<SidebarNavItem
|
||||
key={link.key}
|
||||
isActive={link.highlight(pathname, `/${workspaceSlug}/projects/${projectId}`)}
|
||||
className="text-sm font-medium px-4 py-2"
|
||||
>
|
||||
{t(link.i18n_label)}
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ProjectStateRoot } from "@/components/project-states";
|
||||
// hook
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
const StatesSettingsPage = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// derived values
|
||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - States` : undefined;
|
||||
// derived values
|
||||
const canPerformProjectMemberActions = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
|
||||
if (workspaceUserInfo && !canPerformProjectMemberActions) {
|
||||
return <NotAuthorizedView section="settings" isProjectView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="flex items-center border-b border-custom-border-100">
|
||||
<h3 className="text-xl font-medium">{t("common.states")}</h3>
|
||||
</div>
|
||||
{workspaceSlug && projectId && (
|
||||
<ProjectStateRoot workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default StatesSettingsPage;
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { Settings } from "lucide-react";
|
||||
import { EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Breadcrumbs, CustomMenu, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// hooks
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project";
|
||||
|
||||
export const ProjectSettingHeader: FC = observer(() => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { loader } = useProject();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<div>
|
||||
<div className="z-50">
|
||||
<Breadcrumbs onBack={router.back} isLoading={loader === "init-loader"}>
|
||||
<ProjectBreadcrumb />
|
||||
<div className="hidden sm:hidden md:block lg:block">
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink label="Settings" icon={<Settings className="h-4 w-4 text-custom-text-300" />} />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
<CustomMenu
|
||||
className="flex-shrink-0 block sm:block md:hidden lg:hidden"
|
||||
maxHeight="lg"
|
||||
customButton={
|
||||
<span className="text-xs px-1.5 py-1 border rounded-md text-custom-text-200 border-custom-border-300">
|
||||
Settings
|
||||
</span>
|
||||
}
|
||||
placement="bottom-start"
|
||||
closeOnSelect
|
||||
>
|
||||
{PROJECT_SETTINGS_LINKS.map(
|
||||
(item) =>
|
||||
allowPermissions(
|
||||
item.access,
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString()
|
||||
) && (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.key}
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}${item.href}`)}
|
||||
>
|
||||
{t(item.i18n_label)}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</Header.LeftItem>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
|
|
@ -68,7 +68,7 @@ const ProjectViewsPage = observer(() => {
|
|||
primaryButton={{
|
||||
text: t("disabled_project.empty_state.view.primary_button.text"),
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`);
|
||||
router.push(`/${workspaceSlug}/settings/projects/${projectId}/features`);
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
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 { DetailedEmptyState } from "@/components/empty-state";
|
||||
import { APITokenSettingsLoader } from "@/components/ui";
|
||||
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
// store hooks
|
||||
import { useUserPermissions, useWorkspace } from "@/hooks/store";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
// services
|
||||
import { APITokenService } from "@/services/api_token.service";
|
||||
|
||||
const apiTokenService = new APITokenService();
|
||||
|
||||
const ApiTokensPage = observer(() => {
|
||||
// states
|
||||
const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false);
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/api-tokens" });
|
||||
|
||||
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} - ${t("workspace_settings.settings.api_tokens.title")}`
|
||||
: undefined;
|
||||
|
||||
if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
if (!tokens) {
|
||||
return <APITokenSettingsLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
|
||||
<section className="w-full overflow-y-auto">
|
||||
{tokens.length > 0 ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between border-b border-custom-border-200 pb-3.5">
|
||||
<h3 className="text-xl font-medium">{t("workspace_settings.settings.api_tokens.title")}</h3>
|
||||
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
|
||||
{t("workspace_settings.settings.api_tokens.add_token")}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
{tokens.map((token) => (
|
||||
<ApiTokenListItem key={token.id} token={token} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
|
||||
<h3 className="text-xl font-medium">{t("workspace_settings.settings.api_tokens.title")}</h3>
|
||||
<Button variant="primary" onClick={() => setIsCreateTokenModalOpen(true)}>
|
||||
{t("workspace_settings.settings.api_tokens.add_token")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<DetailedEmptyState
|
||||
title={t("workspace_settings.empty_state.api_tokens.title")}
|
||||
description={t("workspace_settings.empty_state.api_tokens.description")}
|
||||
assetPath={resolvedPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default ApiTokensPage;
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// component
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
// hooks
|
||||
import { useUserPermissions, useWorkspace } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { BillingRoot } from "@/plane-web/components/workspace";
|
||||
|
||||
const BillingSettingsPage = observer(() => {
|
||||
// store hooks
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Billing & Plans` : undefined;
|
||||
|
||||
if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<BillingRoot />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default BillingSettingsPage;
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import ExportGuide from "@/components/exporter/guide";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useUserPermissions, useWorkspace } from "@/hooks/store";
|
||||
|
||||
const ExportsPage = observer(() => {
|
||||
// store hooks
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// derived values
|
||||
const canPerformWorkspaceMemberActions = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
const pageTitle = currentWorkspace?.name
|
||||
? `${currentWorkspace.name} - ${t("workspace_settings.settings.exports.title")}`
|
||||
: undefined;
|
||||
|
||||
// if user is not authorized to view this page
|
||||
if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div
|
||||
className={cn("w-full overflow-y-auto", {
|
||||
"opacity-60": !canPerformWorkspaceMemberActions,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
||||
<h3 className="text-xl font-medium">{t("workspace_settings.settings.exports.title")}</h3>
|
||||
</div>
|
||||
<ExportGuide />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default ExportsPage;
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { PageHead } from "@/components/core";
|
||||
import IntegrationGuide from "@/components/integration/guide";
|
||||
// hooks
|
||||
import { useUserPermissions, useWorkspace } from "@/hooks/store";
|
||||
|
||||
const ImportsPage = observer(() => {
|
||||
// store hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
// derived values
|
||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Imports` : 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>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className="w-full overflow-y-auto">
|
||||
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
||||
<h3 className="text-xl font-medium">Imports</h3>
|
||||
</div>
|
||||
<IntegrationGuide />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default ImportsPage;
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
"use client";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { SingleIntegrationCard } from "@/components/integration";
|
||||
import { IntegrationAndImportExportBanner, IntegrationsSettingsLoader } from "@/components/ui";
|
||||
// constants
|
||||
import { APP_INTEGRATIONS } from "@/constants/fetch-keys";
|
||||
// hooks
|
||||
import { useUserPermissions, useWorkspace } from "@/hooks/store";
|
||||
// services
|
||||
import { IntegrationService } from "@/services/integrations";
|
||||
|
||||
const integrationService = new IntegrationService();
|
||||
|
||||
const WorkspaceIntegrationsPage = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
// derived values
|
||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Integrations` : undefined;
|
||||
|
||||
if (!isAdmin)
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="mt-10 flex h-full w-full justify-center">
|
||||
<p className="text-sm text-custom-text-300">You are not authorized to access this page.</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const { data: appIntegrations } = useSWR(workspaceSlug && isAdmin ? APP_INTEGRATIONS : null, () =>
|
||||
workspaceSlug && isAdmin ? integrationService.getAppIntegrationsList() : null
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className="w-full overflow-y-auto">
|
||||
<IntegrationAndImportExportBanner bannerName="Integrations" />
|
||||
<div>
|
||||
{appIntegrations ? (
|
||||
appIntegrations.map((integration) => (
|
||||
<SingleIntegrationCard key={integration.id} integration={integration} />
|
||||
))
|
||||
) : (
|
||||
<IntegrationsSettingsLoader />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default WorkspaceIntegrationsPage;
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC, ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_ACCESS } from "@plane/constants";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { AppHeader } from "@/components/core";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
// plane web constants
|
||||
// local components
|
||||
import { WorkspaceSettingHeader } from "../header";
|
||||
import { MobileWorkspaceSettingsTabs } from "./mobile-header-tabs";
|
||||
import { WorkspaceSettingsSidebar } from "./sidebar";
|
||||
|
||||
export interface IWorkspaceSettingLayout {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const WorkspaceSettingLayout: FC<IWorkspaceSettingLayout> = observer((props) => {
|
||||
const { children } = props;
|
||||
|
||||
const { workspaceUserInfo } = useUserPermissions();
|
||||
const pathname = usePathname();
|
||||
const [workspaceSlug, suffix, route] = pathname.replace(/^\/|\/$/g, "").split("/"); // Regex removes leading and trailing slashes
|
||||
|
||||
// derived values
|
||||
const userWorkspaceRole = workspaceUserInfo?.[workspaceSlug.toString()]?.role;
|
||||
const isAuthorized =
|
||||
pathname &&
|
||||
workspaceSlug &&
|
||||
userWorkspaceRole &&
|
||||
WORKSPACE_SETTINGS_ACCESS[route ? `/${suffix}/${route}` : `/${suffix}`]?.includes(
|
||||
userWorkspaceRole as EUserWorkspaceRoles
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppHeader header={<WorkspaceSettingHeader />} />
|
||||
<MobileWorkspaceSettingsTabs />
|
||||
<div className="inset-y-0 flex flex-row vertical-scrollbar scrollbar-lg h-full w-full overflow-y-auto">
|
||||
{workspaceUserInfo && !isAuthorized ? (
|
||||
<NotAuthorizedView section="settings" />
|
||||
) : (
|
||||
<>
|
||||
<div className="px-page-x !pr-0 py-page-y flex-shrink-0 overflow-y-hidden sm:hidden hidden md:block lg:block">
|
||||
<WorkspaceSettingsSidebar />
|
||||
</div>
|
||||
<div className="flex flex-col relative w-full overflow-hidden">
|
||||
<div className="w-full h-full overflow-x-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-page-x md:px-9 py-page-y">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default WorkspaceSettingLayout;
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Search } from "lucide-react";
|
||||
// types
|
||||
import { MEMBER_INVITED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IWorkspaceBulkInviteFormData } from "@plane/types";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { CountChip } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { WorkspaceMembersList } from "@/components/workspace";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getUserRole } from "@/helpers/user.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useMember, useUserPermissions, useWorkspace } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { BillingActionsButton } from "@/plane-web/components/workspace/billing";
|
||||
import { SendWorkspaceInvitationModal } from "@/plane-web/components/workspace/members";
|
||||
|
||||
const WorkspaceMembersSettingsPage = observer(() => {
|
||||
// states
|
||||
const [inviteModal, setInviteModal] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
const { captureEvent } = useEventTracker();
|
||||
const {
|
||||
workspace: { workspaceMemberIds, inviteMembersToWorkspace },
|
||||
} = useMember();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// derived values
|
||||
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const canPerformWorkspaceMemberActions = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
|
||||
const handleWorkspaceInvite = (data: IWorkspaceBulkInviteFormData) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
return inviteMembersToWorkspace(workspaceSlug.toString(), data)
|
||||
.then(() => {
|
||||
setInviteModal(false);
|
||||
captureEvent(MEMBER_INVITED, {
|
||||
emails: [
|
||||
...data.emails.map((email) => ({
|
||||
email: email.email,
|
||||
role: getUserRole(email.role as unknown as EUserPermissions),
|
||||
})),
|
||||
],
|
||||
project_id: undefined,
|
||||
state: "SUCCESS",
|
||||
element: "Workspace settings member page",
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: t("workspace_settings.settings.members.invitations_sent_successfully"),
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
captureEvent(MEMBER_INVITED, {
|
||||
emails: [
|
||||
...data.emails.map((email) => ({
|
||||
email: email.email,
|
||||
role: getUserRole(email.role as unknown as EUserPermissions),
|
||||
})),
|
||||
],
|
||||
project_id: undefined,
|
||||
state: "FAILED",
|
||||
element: "Workspace settings member page",
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: `${err.error ?? t("something_went_wrong_please_try_again")}`,
|
||||
});
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined;
|
||||
|
||||
// if user is not authorized to view this page
|
||||
if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<SendWorkspaceInvitationModal
|
||||
isOpen={inviteModal}
|
||||
onClose={() => setInviteModal(false)}
|
||||
onSubmit={handleWorkspaceInvite}
|
||||
/>
|
||||
<section
|
||||
className={cn("w-full h-full overflow-y-auto", {
|
||||
"opacity-60": !canPerformWorkspaceMemberActions,
|
||||
})}
|
||||
>
|
||||
<div className="flex justify-between gap-4 pb-3.5 items-start">
|
||||
<h4 className="flex items-center gap-2.5 text-xl font-medium">
|
||||
{t("workspace_settings.settings.members.title")}
|
||||
{workspaceMemberIds && workspaceMemberIds.length > 0 && (
|
||||
<CountChip count={workspaceMemberIds.length} className="h-5 m-auto" />
|
||||
)}
|
||||
</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">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" />
|
||||
<input
|
||||
className="w-full max-w-[234px] border-none bg-transparent text-sm outline-none placeholder:text-custom-text-400"
|
||||
placeholder={`${t("search")}...`}
|
||||
value={searchQuery}
|
||||
autoFocus
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{canPerformWorkspaceAdminActions && (
|
||||
<Button variant="primary" size="sm" onClick={() => setInviteModal(true)}>
|
||||
{t("workspace_settings.settings.members.add_member")}
|
||||
</Button>
|
||||
)}
|
||||
<BillingActionsButton canPerformWorkspaceAdminActions={canPerformWorkspaceAdminActions} />
|
||||
</div>
|
||||
<WorkspaceMembersList searchQuery={searchQuery} isAdmin={canPerformWorkspaceAdminActions} />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default WorkspaceMembersSettingsPage;
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { WORKSPACE_SETTINGS_LINKS, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web helpers
|
||||
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
|
||||
|
||||
export const MobileWorkspaceSettingsTabs = observer(() => {
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug } = useParams();
|
||||
const pathname = usePathname();
|
||||
const { t } = useTranslation();
|
||||
// mobx store
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
return (
|
||||
<div className="flex-shrink-0 md:hidden sticky inset-0 flex overflow-x-auto bg-custom-background-100 z-10">
|
||||
{WORKSPACE_SETTINGS_LINKS.map(
|
||||
(item, index) =>
|
||||
shouldRenderSettingLink(workspaceSlug.toString(), item.key) &&
|
||||
allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
|
||||
<div
|
||||
className={`${
|
||||
item.highlight(pathname, `/${workspaceSlug}`)
|
||||
? "text-custom-primary-100 text-sm py-2 px-3 whitespace-nowrap flex flex-grow cursor-pointer justify-around border-b border-custom-primary-200"
|
||||
: "text-custom-text-200 flex flex-grow cursor-pointer justify-around border-b border-custom-border-200 text-sm py-2 px-3 whitespace-nowrap"
|
||||
}`}
|
||||
key={index}
|
||||
onClick={() => router.push(`/${workspaceSlug}${item.href}`)}
|
||||
>
|
||||
{t(item.i18n_label)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { WorkspaceDetails } from "@/components/workspace";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
||||
const WorkspaceSettingsPage = observer(() => {
|
||||
// store hooks
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name
|
||||
? t("workspace_settings.page_label", { workspace: currentWorkspace.name })
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<WorkspaceDetails />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default WorkspaceSettingsPage;
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { WORKSPACE_SETTINGS_LINKS, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { SidebarNavItem } from "@/components/sidebar";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
// plane web helpers
|
||||
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
|
||||
|
||||
export const WorkspaceSettingsSidebar = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
const pathname = usePathname();
|
||||
// mobx store
|
||||
const { t } = useTranslation();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
return (
|
||||
<div className="flex w-[280px] flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-semibold text-custom-sidebar-text-400 uppercase">{t("settings")}</span>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
{WORKSPACE_SETTINGS_LINKS.map(
|
||||
(link) =>
|
||||
shouldRenderSettingLink(workspaceSlug.toString(), link.key) &&
|
||||
allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && (
|
||||
<Link key={link.key} href={`/${workspaceSlug}${link.href}`}>
|
||||
<SidebarNavItem
|
||||
key={link.key}
|
||||
isActive={link.highlight(pathname, `/${workspaceSlug}`)}
|
||||
className="text-sm font-medium px-4 py-2"
|
||||
>
|
||||
{t(link.i18n_label)}
|
||||
</SidebarNavItem>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { IWebhook } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "@/components/web-hooks";
|
||||
// hooks
|
||||
import { useUserPermissions, useWebhook, useWorkspace } from "@/hooks/store";
|
||||
|
||||
const WebhookDetailsPage = observer(() => {
|
||||
// states
|
||||
const [deleteWebhookModal, setDeleteWebhookModal] = useState(false);
|
||||
// router
|
||||
const { workspaceSlug, webhookId } = useParams();
|
||||
// mobx store
|
||||
const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
// TODO: fix this error
|
||||
// useEffect(() => {
|
||||
// if (isCreated !== "true") clearSecretKey();
|
||||
// }, [clearSecretKey, isCreated]);
|
||||
|
||||
// derived values
|
||||
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Webhook` : undefined;
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && webhookId && isAdmin ? `WEBHOOK_DETAILS_${workspaceSlug}_${webhookId}` : null,
|
||||
workspaceSlug && webhookId && isAdmin
|
||||
? () => fetchWebhookById(workspaceSlug.toString(), webhookId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const handleUpdateWebhook = async (formData: IWebhook) => {
|
||||
if (!workspaceSlug || !formData || !formData.id) return;
|
||||
const payload = {
|
||||
url: formData?.url,
|
||||
is_active: formData?.is_active,
|
||||
project: formData?.project,
|
||||
cycle: formData?.cycle,
|
||||
module: formData?.module,
|
||||
issue: formData?.issue,
|
||||
issue_comment: formData?.issue_comment,
|
||||
};
|
||||
await updateWebhook(workspaceSlug.toString(), formData.id, payload)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Webhook updated successfully.",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: error?.error ?? "Something went wrong. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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 (!currentWebhook)
|
||||
return (
|
||||
<div className="grid h-full w-full place-items-center p-4">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} />
|
||||
<div className="w-full space-y-8 overflow-y-auto">
|
||||
<div className="">
|
||||
<WebhookForm onSubmit={async (data) => await handleUpdateWebhook(data)} data={currentWebhook} />
|
||||
</div>
|
||||
{currentWebhook && <WebhookDeleteSection openDeleteModal={() => setDeleteWebhookModal(true)} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default WebhookDetailsPage;
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/ui";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { DetailedEmptyState } from "@/components/empty-state";
|
||||
import { WebhookSettingsLoader } from "@/components/ui";
|
||||
import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks";
|
||||
// hooks
|
||||
import { useUserPermissions, useWebhook, useWorkspace } from "@/hooks/store";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
const WebhooksListPage = observer(() => {
|
||||
// states
|
||||
const [showCreateWebhookModal, setShowCreateWebhookModal] = useState(false);
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// mobx store
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/webhooks" });
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null,
|
||||
workspaceSlug && canPerformWorkspaceAdminActions ? () => fetchWebhooks(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
const pageTitle = currentWorkspace?.name
|
||||
? `${currentWorkspace.name} - ${t("workspace_settings.settings.webhooks.title")}`
|
||||
: undefined;
|
||||
|
||||
// clear secret key when modal is closed.
|
||||
useEffect(() => {
|
||||
if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey();
|
||||
}, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]);
|
||||
|
||||
if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
|
||||
return <NotAuthorizedView section="settings" />;
|
||||
}
|
||||
|
||||
if (!webhooks) return <WebhookSettingsLoader />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="w-full overflow-y-auto">
|
||||
<CreateWebhookModal
|
||||
createWebhook={createWebhook}
|
||||
clearSecretKey={clearSecretKey}
|
||||
currentWorkspace={currentWorkspace}
|
||||
isOpen={showCreateWebhookModal}
|
||||
onClose={() => {
|
||||
setShowCreateWebhookModal(false);
|
||||
}}
|
||||
/>
|
||||
{Object.keys(webhooks).length > 0 ? (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
|
||||
<div className="text-xl font-medium">{t("workspace_settings.settings.webhooks.title")}</div>
|
||||
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
|
||||
{t("workspace_settings.settings.webhooks.add_webhook")}
|
||||
</Button>
|
||||
</div>
|
||||
<WebhooksList />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-200 pb-3.5">
|
||||
<div className="text-xl font-medium">{t("workspace_settings.settings.webhooks.title")}</div>
|
||||
<Button variant="primary" size="sm" onClick={() => setShowCreateWebhookModal(true)}>
|
||||
{t("workspace_settings.settings.webhooks.add_webhook")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<DetailedEmptyState
|
||||
title={t("workspace_settings.empty_state.webhooks.title")}
|
||||
description={t("workspace_settings.empty_state.webhooks.description")}
|
||||
assetPath={resolvedPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default WebhooksListPage;
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Settings } from "lucide-react";
|
||||
// ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Breadcrumbs, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
||||
export const WorkspaceSettingHeader: FC = observer(() => {
|
||||
const { currentWorkspace, loader } = useWorkspace();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs isLoading={loader}>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href={`/${currentWorkspace?.slug}/settings`}
|
||||
label={currentWorkspace?.name ?? "Workspace"}
|
||||
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label={t("settings")} />} />
|
||||
</Breadcrumbs>
|
||||
</Header.LeftItem>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue