This commit is contained in:
DCCONSTRUCTIONS
2026-04-18 18:39:25 +03:00
commit 3ba092b60c
4944 changed files with 497564 additions and 0 deletions
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { Outlet } from "react-router";
// components
import { ContentWrapper } from "@/components/core/content-wrapper";
import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider";
export default function SettingsLayout() {
return (
<>
<ProjectsAppPowerKProvider />
<div className="relative flex size-full overflow-hidden rounded-lg border border-subtle">
<main className="relative flex size-full flex-col overflow-hidden">
{/* Content */}
<ContentWrapper className="w-full bg-surface-1 md:flex">
<div className="size-full overflow-hidden">
<Outlet />
</div>
</ContentWrapper>
</main>
</div>
</>
);
}
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { WORKSPACE_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon";
export const BillingWorkspaceSettingsHeader = observer(function BillingWorkspaceSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = WORKSPACE_SETTINGS["billing-and-plans"];
const Icon = WORKSPACE_SETTINGS_ICONS["billing-and-plans"];
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { LogoSpinner } from "@/components/common/logo-spinner";
import { useAppRouter } from "@/hooks/use-app-router";
function BillingSettingsPage() {
const router = useAppRouter();
const { workspaceSlug } = useParams();
useEffect(() => {
if (workspaceSlug) {
router.replace(`/${workspaceSlug}/settings`);
}
}, [router, workspaceSlug]);
return (
<div className="grid h-full place-items-center">
<LogoSpinner />
</div>
);
}
export default observer(BillingSettingsPage);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { WORKSPACE_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon";
export const ExportsWorkspaceSettingsHeader = observer(function ExportsWorkspaceSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = WORKSPACE_SETTINGS.export;
const Icon = WORKSPACE_SETTINGS_ICONS.export;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { cn } from "@plane/utils";
// components
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { ExportGuide } from "@/components/exporter/guide";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { SettingsHeading } from "@/components/settings/heading";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import { ExportsWorkspaceSettingsHeader } from "./header";
function ExportsPage() {
// 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" className="h-auto" />;
}
return (
<SettingsContentWrapper header={<ExportsWorkspaceSettingsHeader />} hugging>
<PageHead title={pageTitle} />
<div
className={cn("flex w-full flex-col gap-y-6", {
"opacity-60": !canPerformWorkspaceMemberActions,
})}
>
<SettingsHeading
title={t("workspace_settings.settings.exports.heading")}
description={t("workspace_settings.settings.exports.description")}
/>
<ExportGuide />
</div>
</SettingsContentWrapper>
);
}
export default observer(ExportsPage);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { WORKSPACE_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon";
export const GeneralWorkspaceSettingsHeader = observer(function GeneralWorkspaceSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = WORKSPACE_SETTINGS.general;
const Icon = WORKSPACE_SETTINGS_ICONS.general;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import useSWR from "swr";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { SingleIntegrationCard } from "@/components/integration/single-integration-card";
import { IntegrationAndImportExportBanner } from "@/components/ui/integration-and-import-export-banner";
import { IntegrationsSettingsLoader } from "@/components/ui/loader/settings/integration";
// constants
import { APP_INTEGRATIONS } from "@/constants/fetch-keys";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUserPermissions } from "@/hooks/store/user";
// services
import { IntegrationService } from "@/services/integrations";
const integrationService = new IntegrationService();
function WorkspaceIntegrationsPage() {
// 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;
const { data: appIntegrations } = useSWR(isAdmin ? APP_INTEGRATIONS : null, () =>
isAdmin ? integrationService.getAppIntegrationsList() : null
);
if (!isAdmin) return <NotAuthorizedView section="settings" className="h-auto" />;
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 observer(WorkspaceIntegrationsPage);
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { Outlet } from "react-router";
// components
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { getWorkspaceActivePath, pathnameToAccessKey } from "@/components/settings/helper";
import { SettingsMobileNav } from "@/components/settings/mobile/nav";
// plane imports
import { WORKSPACE_SETTINGS_ACCESS } from "@plane/constants";
import type { EUserWorkspaceRoles } from "@plane/types";
// components
import { WorkspaceSettingsSidebarRoot } from "@/components/settings/workspace/sidebar";
// hooks
import { useUserPermissions } from "@/hooks/store/user";
import type { Route } from "./+types/layout";
const WorkspaceSettingLayout = observer(function WorkspaceSettingLayout({ params }: Route.ComponentProps) {
// router
const { workspaceSlug } = params;
// store hooks
const { workspaceUserInfo, getWorkspaceRoleByWorkspaceSlug } = useUserPermissions();
// next hooks
const pathname = usePathname();
// derived values
const { accessKey } = pathnameToAccessKey(pathname);
const userWorkspaceRole = getWorkspaceRoleByWorkspaceSlug(workspaceSlug);
let isAuthorized: boolean | string = false;
if (pathname && workspaceSlug && userWorkspaceRole) {
isAuthorized = WORKSPACE_SETTINGS_ACCESS[accessKey]?.includes(userWorkspaceRole as EUserWorkspaceRoles);
}
return (
<>
<SettingsMobileNav
hamburgerContent={WorkspaceSettingsSidebarRoot}
activePath={getWorkspaceActivePath(pathname) || ""}
/>
<div className="inset-y-0 flex h-full w-full flex-row">
{workspaceUserInfo && !isAuthorized ? (
<NotAuthorizedView section="settings" className="h-auto" />
) : (
<div className="relative flex size-full">
<div className="hidden h-full md:block">
<WorkspaceSettingsSidebarRoot />
</div>
<Outlet />
</div>
)}
</div>
</>
);
});
export default WorkspaceSettingLayout;
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { WORKSPACE_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon";
export const MembersWorkspaceSettingsHeader = observer(function MembersWorkspaceSettingsHeader() {
// plane hooks
const { t } = useTranslation();
// derived values
const settingsDetails = WORKSPACE_SETTINGS.members;
const Icon = WORKSPACE_SETTINGS_ICONS.members;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,155 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import { observer } from "mobx-react";
// types
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { SearchIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IWorkspaceBulkInviteFormData } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { CountChip } from "@/components/common/count-chip";
import { PageHead } from "@/components/core/page-title";
import { MemberListFiltersDropdown } from "@/components/project/dropdowns/filters/member-list";
import { WorkspaceMembersList } from "@/components/workspace/settings/members-list";
// hooks
import { useMember } from "@/hooks/store/use-member";
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUserPermissions } from "@/hooks/store/user";
// plane web components
import { BillingActionsButton } from "@/plane-web/components/workspace/billing/billing-actions-button";
import { SendWorkspaceInvitationModal, MembersActivityButton } from "@/plane-web/components/workspace/members";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
// local imports
import type { Route } from "./+types/page";
import { MembersWorkspaceSettingsHeader } from "./header";
const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsPage({ params }: Route.ComponentProps) {
// states
const [inviteModal, setInviteModal] = useState(false);
const [searchQuery, setSearchQuery] = useState<string>("");
// router
const { workspaceSlug } = params;
// store hooks
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const {
workspace: { workspaceMemberIds, inviteMembersToWorkspace, filtersStore },
} = 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 = async (data: IWorkspaceBulkInviteFormData) => {
try {
await inviteMembersToWorkspace(workspaceSlug, data);
setInviteModal(false);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: t("workspace_settings.settings.members.invitations_sent_successfully"),
});
} catch (error: unknown) {
let message = undefined;
if (error instanceof Error) {
const err = error as Error & { error?: string };
message = err.error;
}
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: `${message ?? t("something_went_wrong_please_try_again")}`,
});
throw error;
}
};
// Handler for role filter updates
const handleRoleFilterUpdate = (role: string) => {
const currentFilters = filtersStore.filters;
const currentRoles = currentFilters?.roles || [];
const updatedRoles = currentRoles.includes(role) ? currentRoles.filter((r) => r !== role) : [...currentRoles, role];
filtersStore.updateFilters({
roles: updatedRoles.length > 0 ? updatedRoles : undefined,
});
};
// derived values
const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Members` : undefined;
const appliedRoleFilters = filtersStore.filters?.roles || [];
// if user is not authorized to view this page
if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
return <NotAuthorizedView section="settings" className="h-auto" />;
}
return (
<SettingsContentWrapper header={<MembersWorkspaceSettingsHeader />} hugging>
<PageHead title={pageTitle} />
<SendWorkspaceInvitationModal
isOpen={inviteModal}
onClose={() => setInviteModal(false)}
onSubmit={handleWorkspaceInvite}
/>
<section
className={cn("size-full", {
"opacity-60": !canPerformWorkspaceMemberActions,
})}
>
<div className="flex items-center justify-between gap-4 pb-3.5">
<h4 className="flex items-center gap-2.5 text-h3-medium">
{t("workspace_settings.settings.members.title")}
{workspaceMemberIds && workspaceMemberIds.length > 0 && (
<CountChip count={workspaceMemberIds.length} className="m-auto h-5" />
)}
</h4>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5 rounded-md border border-subtle bg-surface-1 px-2.5 py-1.5">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" />
<input
className="w-full max-w-[234px] border-none bg-transparent text-body-xs-regular outline-none placeholder:text-placeholder"
placeholder={`${t("search")}...`}
value={searchQuery}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<MemberListFiltersDropdown
appliedFilters={appliedRoleFilters}
handleUpdate={handleRoleFilterUpdate}
memberType="workspace"
/>
<MembersActivityButton workspaceSlug={workspaceSlug} />
{canPerformWorkspaceAdminActions && (
<Button variant="primary" size="lg" onClick={() => setInviteModal(true)}>
{t("workspace_settings.settings.members.add_member")}
</Button>
)}
<BillingActionsButton canPerformWorkspaceAdminActions={canPerformWorkspaceAdminActions} />
</div>
</div>
<WorkspaceMembersList searchQuery={searchQuery} isAdmin={canPerformWorkspaceAdminActions} />
</section>
</SettingsContentWrapper>
);
});
export default WorkspaceMembersSettingsPage;
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
// components
import { PageHead } from "@/components/core/page-title";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { WorkspaceDetails } from "@/components/workspace/settings/workspace-details";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
// local imports
import { GeneralWorkspaceSettingsHeader } from "./header";
function GeneralWorkspaceSettingsPage() {
// store hooks
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
// derived values
const pageTitle = currentWorkspace?.name
? t("workspace_settings.page_label", { workspace: currentWorkspace.name })
: undefined;
return (
<SettingsContentWrapper header={<GeneralWorkspaceSettingsHeader />}>
<PageHead title={pageTitle} />
<WorkspaceDetails />
</SettingsContentWrapper>
);
}
export default observer(GeneralWorkspaceSettingsPage);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { WORKSPACE_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon";
export const WebhookDetailsWorkspaceSettingsHeader = observer(function WebhookDetailsWorkspaceSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = WORKSPACE_SETTINGS.webhooks;
const Icon = WORKSPACE_SETTINGS_ICONS.webhooks;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,111 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IWebhook } from "@plane/types";
// ui
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { PageHead } from "@/components/core/page-title";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "@/components/web-hooks";
// hooks
import { useWebhook } from "@/hooks/store/use-webhook";
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import type { Route } from "./+types/page";
import { WebhookDetailsWorkspaceSettingsHeader } from "./header";
function WebhookDetailsPage({ params }: Route.ComponentProps) {
// states
const [deleteWebhookModal, setDeleteWebhookModal] = useState(false);
// router
const { workspaceSlug, webhookId } = params;
// 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(
isAdmin ? `WEBHOOK_DETAILS_${workspaceSlug}_${webhookId}` : null,
isAdmin ? () => fetchWebhookById(workspaceSlug, webhookId) : null
);
const handleUpdateWebhook = async (formData: IWebhook) => {
if (!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,
};
try {
await updateWebhook(workspaceSlug, formData.id, payload);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Webhook updated successfully.",
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
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-13 text-tertiary">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 (
<SettingsContentWrapper header={<WebhookDetailsWorkspaceSettingsHeader />}>
<PageHead title={pageTitle} />
<DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} />
<div className="w-full space-y-8 overflow-y-auto">
<div>
<WebhookForm onSubmit={handleUpdateWebhook} data={currentWebhook} />
</div>
{currentWebhook && <WebhookDeleteSection openDeleteModal={() => setDeleteWebhookModal(true)} />}
</div>
</SettingsContentWrapper>
);
}
export default observer(WebhookDetailsPage);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { WORKSPACE_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon";
export const WebhooksWorkspaceSettingsHeader = observer(function WebhooksWorkspaceSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = WORKSPACE_SETTINGS.webhooks;
const Icon = WORKSPACE_SETTINGS_ICONS.webhooks;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
// components
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { SettingsHeading } from "@/components/settings/heading";
import { WebhookSettingsLoader } from "@/components/ui/loader/settings/web-hook";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks";
// hooks
import { useWebhook } from "@/hooks/store/use-webhook";
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import type { Route } from "./+types/page";
import { WebhooksWorkspaceSettingsHeader } from "./header";
function WebhooksListPage({ params }: Route.ComponentProps) {
// states
const [showCreateWebhookModal, setShowCreateWebhookModal] = useState(false);
// router
const { workspaceSlug } = params;
// 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);
useSWR(
canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null,
canPerformWorkspaceAdminActions ? () => fetchWebhooks(workspaceSlug) : 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" className="h-auto" />;
}
if (!webhooks) return <WebhookSettingsLoader />;
return (
<SettingsContentWrapper header={<WebhooksWorkspaceSettingsHeader />}>
<PageHead title={pageTitle} />
<div className="w-full">
<CreateWebhookModal
createWebhook={createWebhook}
clearSecretKey={clearSecretKey}
currentWorkspace={currentWorkspace}
isOpen={showCreateWebhookModal}
onClose={() => {
setShowCreateWebhookModal(false);
}}
/>
<SettingsHeading
title={t("workspace_settings.settings.webhooks.title")}
description={t("workspace_settings.settings.webhooks.description")}
control={
<Button variant="primary" size="lg" onClick={() => setShowCreateWebhookModal(true)}>
{t("workspace_settings.settings.webhooks.add_webhook")}
</Button>
}
/>
{Object.keys(webhooks).length > 0 ? (
<div className="mt-4">
<WebhooksList />
</div>
) : (
<div className="flex h-full w-full flex-col">
<div className="flex h-full w-full items-center justify-center">
<EmptyStateCompact
assetKey="webhook"
title={t("settings_empty_state.webhooks.title")}
description={t("settings_empty_state.webhooks.description")}
actions={[
{
label: t("settings_empty_state.webhooks.cta_primary"),
onClick: () => {
setShowCreateWebhookModal(true);
},
},
]}
align="start"
rootClassName="py-20"
/>
</div>
</div>
)}
</div>
</SettingsContentWrapper>
);
}
export default observer(WebhooksListPage);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { PROJECT_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon";
export const AutomationsProjectSettingsHeader = observer(function AutomationsProjectSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = PROJECT_SETTINGS.automations;
const Icon = PROJECT_SETTINGS_ICONS.automations;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import { Outlet } from "react-router";
// plane web imports
import { AutomationsListWrapper } from "@/plane-web/components/automations/list/wrapper";
import type { Route } from "./+types/layout";
function AutomationsListLayout({ params }: Route.ComponentProps) {
const { projectId, workspaceSlug } = params;
return (
<AutomationsListWrapper projectId={projectId} workspaceSlug={workspaceSlug}>
<Outlet />
</AutomationsListWrapper>
);
}
export default observer(AutomationsListLayout);
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IProject } from "@plane/types";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automation";
import { PageHead } from "@/components/core/page-title";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { SettingsHeading } from "@/components/settings/heading";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
// plane web imports
import { CustomAutomationsRoot } from "@/plane-web/components/automations/root";
// local imports
import type { Route } from "./+types/page";
import { AutomationsProjectSettingsHeader } from "./header";
function AutomationSettingsPage({ params }: Route.ComponentProps) {
// router
const { workspaceSlug, projectId } = params;
// 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 (!projectDetails) return;
try {
await updateProject(workspaceSlug, projectId, 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 className="h-auto" />;
}
return (
<SettingsContentWrapper header={<AutomationsProjectSettingsHeader />} hugging>
<PageHead title={pageTitle} />
<section className={`w-full ${canPerformProjectAdminActions ? "" : "opacity-60"}`}>
<SettingsHeading
title={t("project_settings.automations.heading")}
description={t("project_settings.automations.description")}
/>
<div className="mt-6">
<AutoArchiveAutomation handleChange={handleChange} />
<AutoCloseAutomation handleChange={handleChange} />
</div>
</section>
<CustomAutomationsRoot projectId={projectId} workspaceSlug={workspaceSlug} />
</SettingsContentWrapper>
);
}
export default observer(AutomationSettingsPage);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { PROJECT_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon";
export const EstimatesProjectSettingsHeader = observer(function EstimatesProjectSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = PROJECT_SETTINGS.estimates;
const Icon = PROJECT_SETTINGS_ICONS.estimates;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { EstimateRoot } from "@/components/estimates";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import type { Route } from "./+types/page";
import { EstimatesProjectSettingsHeader } from "./header";
function EstimatesSettingsPage({ params }: Route.ComponentProps) {
const { workspaceSlug, projectId } = params;
// 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 (workspaceUserInfo && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
}
return (
<SettingsContentWrapper header={<EstimatesProjectSettingsHeader />}>
<PageHead title={pageTitle} />
<div className={`w-full ${canPerformProjectAdminActions ? "" : "pointer-events-none opacity-60"}`}>
<EstimateRoot workspaceSlug={workspaceSlug} projectId={projectId} isAdmin={canPerformProjectAdminActions} />
</div>
</SettingsContentWrapper>
);
}
export default observer(EstimatesSettingsPage);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { PROJECT_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon";
export const FeaturesCyclesProjectSettingsHeader = observer(function FeaturesCyclesProjectSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = PROJECT_SETTINGS.features_cycles;
const Icon = PROJECT_SETTINGS_ICONS.features_cycles;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import type { Route } from "./+types/page";
import { FeaturesCyclesProjectSettingsHeader } from "./header";
import { SettingsHeading } from "@/components/settings/heading";
function FeaturesCyclesSettingsPage({ params }: Route.ComponentProps) {
const { workspaceSlug, projectId } = params;
// store hooks
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { currentProjectDetails } = useProject();
// translation
const { t } = useTranslation();
// derived values
const pageTitle = currentProjectDetails?.name
? `${currentProjectDetails?.name} settings - ${t("project_settings.features.cycles.short_title")}`
: undefined;
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
if (workspaceUserInfo && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
}
return (
<SettingsContentWrapper header={<FeaturesCyclesProjectSettingsHeader />}>
<PageHead title={pageTitle} />
<section className="w-full">
<SettingsHeading
title={t("project_settings.features.cycles.title")}
description={t("project_settings.features.cycles.description")}
/>
<div className="mt-7">
<ProjectSettingsFeatureControlItem
title={t("project_settings.features.cycles.toggle_title")}
description={t("project_settings.features.cycles.toggle_description")}
featureProperty="cycle_view"
projectId={projectId}
value={!!currentProjectDetails?.cycle_view}
workspaceSlug={workspaceSlug}
/>
</div>
</section>
</SettingsContentWrapper>
);
}
export default observer(FeaturesCyclesSettingsPage);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { PROJECT_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon";
export const FeaturesIntakeProjectSettingsHeader = observer(function FeaturesIntakeProjectSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = PROJECT_SETTINGS.features_intake;
const Icon = PROJECT_SETTINGS_ICONS.features_intake;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { SettingsHeading } from "@/components/settings/heading";
import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import type { Route } from "./+types/page";
import { FeaturesIntakeProjectSettingsHeader } from "./header";
function FeaturesIntakeSettingsPage({ params }: Route.ComponentProps) {
const { workspaceSlug, projectId } = params;
// store hooks
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { currentProjectDetails } = useProject();
// translation
const { t } = useTranslation();
// derived values
const pageTitle = currentProjectDetails?.name
? `${currentProjectDetails?.name} settings - ${t("project_settings.features.intake.short_title")}`
: undefined;
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
if (workspaceUserInfo && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
}
return (
<SettingsContentWrapper header={<FeaturesIntakeProjectSettingsHeader />}>
<PageHead title={pageTitle} />
<section className="w-full">
<SettingsHeading
title={t("project_settings.features.intake.title")}
description={t("project_settings.features.intake.description")}
/>
<div className="mt-7">
<ProjectSettingsFeatureControlItem
title={t("project_settings.features.intake.toggle_title")}
description={t("project_settings.features.intake.toggle_description")}
featureProperty="inbox_view"
projectId={projectId}
value={!!currentProjectDetails?.inbox_view}
workspaceSlug={workspaceSlug}
/>
</div>
</section>
</SettingsContentWrapper>
);
}
export default observer(FeaturesIntakeSettingsPage);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { PROJECT_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon";
export const FeaturesModulesProjectSettingsHeader = observer(function FeaturesModulesProjectSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = PROJECT_SETTINGS.features_modules;
const Icon = PROJECT_SETTINGS_ICONS.features_modules;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { SettingsHeading } from "@/components/settings/heading";
import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import type { Route } from "./+types/page";
import { FeaturesModulesProjectSettingsHeader } from "./header";
function FeaturesModulesSettingsPage({ params }: Route.ComponentProps) {
const { workspaceSlug, projectId } = params;
// store hooks
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { currentProjectDetails } = useProject();
// translation
const { t } = useTranslation();
// derived values
const pageTitle = currentProjectDetails?.name
? `${currentProjectDetails?.name} settings - ${t("project_settings.features.modules.short_title")}`
: undefined;
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
if (workspaceUserInfo && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
}
return (
<SettingsContentWrapper header={<FeaturesModulesProjectSettingsHeader />}>
<PageHead title={pageTitle} />
<section className="w-full">
<SettingsHeading
title={t("project_settings.features.modules.title")}
description={t("project_settings.features.modules.description")}
/>
<div className="mt-7">
<ProjectSettingsFeatureControlItem
title={t("project_settings.features.modules.toggle_title")}
description={t("project_settings.features.modules.toggle_description")}
featureProperty="module_view"
projectId={projectId}
value={!!currentProjectDetails?.module_view}
workspaceSlug={workspaceSlug}
/>
</div>
</section>
</SettingsContentWrapper>
);
}
export default observer(FeaturesModulesSettingsPage);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { PROJECT_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon";
export const FeaturesPagesProjectSettingsHeader = observer(function FeaturesPagesProjectSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = PROJECT_SETTINGS.features_pages;
const Icon = PROJECT_SETTINGS_ICONS.features_pages;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { SettingsHeading } from "@/components/settings/heading";
import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import type { Route } from "./+types/page";
import { FeaturesPagesProjectSettingsHeader } from "./header";
function FeaturesPagesSettingsPage({ params }: Route.ComponentProps) {
const { workspaceSlug, projectId } = params;
// store hooks
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { currentProjectDetails } = useProject();
// translation
const { t } = useTranslation();
// derived values
const pageTitle = currentProjectDetails?.name
? `${currentProjectDetails?.name} settings - ${t("project_settings.features.pages.short_title")}`
: undefined;
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
if (workspaceUserInfo && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
}
return (
<SettingsContentWrapper header={<FeaturesPagesProjectSettingsHeader />}>
<PageHead title={pageTitle} />
<section className="w-full">
<SettingsHeading
title={t("project_settings.features.pages.title")}
description={t("project_settings.features.pages.description")}
/>
<div className="mt-7">
<ProjectSettingsFeatureControlItem
title={t("project_settings.features.pages.toggle_title")}
description={t("project_settings.features.pages.toggle_description")}
featureProperty="page_view"
projectId={projectId}
value={!!currentProjectDetails?.page_view}
workspaceSlug={workspaceSlug}
/>
</div>
</section>
</SettingsContentWrapper>
);
}
export default observer(FeaturesPagesSettingsPage);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { PROJECT_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon";
export const FeaturesViewsProjectSettingsHeader = observer(function FeaturesViewsProjectSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = PROJECT_SETTINGS.features_views;
const Icon = PROJECT_SETTINGS_ICONS.features_views;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { SettingsHeading } from "@/components/settings/heading";
import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import type { Route } from "./+types/page";
import { FeaturesViewsProjectSettingsHeader } from "./header";
function FeaturesViewsSettingsPage({ params }: Route.ComponentProps) {
const { workspaceSlug, projectId } = params;
// store hooks
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { currentProjectDetails } = useProject();
// translation
const { t } = useTranslation();
// derived values
const pageTitle = currentProjectDetails?.name
? `${currentProjectDetails?.name} settings - ${t("project_settings.features.views.short_title")}`
: undefined;
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
if (workspaceUserInfo && !canPerformProjectAdminActions) {
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
}
return (
<SettingsContentWrapper header={<FeaturesViewsProjectSettingsHeader />}>
<PageHead title={pageTitle} />
<section className="w-full">
<SettingsHeading
title={t("project_settings.features.views.title")}
description={t("project_settings.features.views.description")}
/>
<div className="mt-7">
<ProjectSettingsFeatureControlItem
title={t("project_settings.features.views.toggle_title")}
description={t("project_settings.features.views.toggle_description")}
featureProperty="issue_views_view"
projectId={projectId}
value={!!currentProjectDetails?.issue_views_view}
workspaceSlug={workspaceSlug}
/>
</div>
</section>
</SettingsContentWrapper>
);
}
export default observer(FeaturesViewsSettingsPage);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { PROJECT_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon";
export const GeneralProjectSettingsHeader = observer(function GeneralProjectSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = PROJECT_SETTINGS.general;
const Icon = PROJECT_SETTINGS_ICONS.general;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { PROJECT_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon";
export const LabelsProjectSettingsHeader = observer(function LabelsProjectSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = PROJECT_SETTINGS.labels;
const Icon = PROJECT_SETTINGS_ICONS.labels;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,65 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
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/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { ProjectSettingsLabelList } from "@/components/labels";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import { LabelsProjectSettingsHeader } from "./header";
function LabelsSettingsPage() {
// 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,
})
);
}, []);
if (workspaceUserInfo && !canPerformProjectMemberActions) {
return <NotAuthorizedView section="settings" isProjectView className="h-auto" />;
}
return (
<SettingsContentWrapper header={<LabelsProjectSettingsHeader />}>
<PageHead title={pageTitle} />
<div ref={scrollableContainerRef} className="size-full">
<ProjectSettingsLabelList />
</div>
</SettingsContentWrapper>
);
}
export default observer(LabelsSettingsPage);
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { Outlet } from "react-router";
// components
import { getProjectActivePath } from "@/components/settings/helper";
import { SettingsMobileNav } from "@/components/settings/mobile/nav";
// layouts
import { ProjectAuthWrapper } from "@/layouts/auth-layout/project-wrapper";
// types
import type { Route } from "./+types/layout";
import { ProjectSettingsSidebarRoot } from "@/components/settings/project/sidebar";
function ProjectDetailSettingsLayout({ params }: Route.ComponentProps) {
const { workspaceSlug, projectId } = params;
// router
const pathname = usePathname();
return (
<>
<SettingsMobileNav
hamburgerContent={(props) => <ProjectSettingsSidebarRoot {...props} projectId={projectId} />}
activePath={getProjectActivePath(pathname) || ""}
/>
<div className="inset-y-0 flex h-full w-full flex-row">
<div className="relative flex size-full">
<div className="hidden h-full shrink-0 md:block">
<ProjectSettingsSidebarRoot projectId={projectId} />
</div>
<ProjectAuthWrapper workspaceSlug={workspaceSlug} projectId={projectId}>
<Outlet />
</ProjectAuthWrapper>
</div>
</div>
</>
);
}
export default observer(ProjectDetailSettingsLayout);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { PROJECT_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon";
export const MembersProjectSettingsHeader = observer(function MembersProjectSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = PROJECT_SETTINGS.members;
const Icon = PROJECT_SETTINGS_ICONS.members;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { ProjectMemberList } from "@/components/project/member-list";
import { ProjectSettingsMemberDefaults } from "@/components/project/project-settings-member-defaults";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { SettingsHeading } from "@/components/settings/heading";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
// plane web imports
import { ProjectTeamspaceList } from "@/plane-web/components/projects/teamspaces/teamspace-list";
// local imports
import type { Route } from "./+types/page";
import { MembersProjectSettingsHeader } from "./header";
function MembersSettingsPage({ params }: Route.ComponentProps) {
// router
const { workspaceSlug, projectId } = params;
// plane hooks
const { t } = useTranslation();
// store hooks
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 className="h-auto" />;
}
return (
<SettingsContentWrapper header={<MembersProjectSettingsHeader />} hugging>
<PageHead title={pageTitle} />
<SettingsHeading title={t("common.members")} />
<ProjectSettingsMemberDefaults projectId={projectId} workspaceSlug={workspaceSlug} />
<ProjectTeamspaceList projectId={projectId} workspaceSlug={workspaceSlug} />
<ProjectMemberList projectId={projectId} workspaceSlug={workspaceSlug} />
</SettingsContentWrapper>
);
}
export default observer(MembersSettingsPage);
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
// components
import { PageHead } from "@/components/core/page-title";
import { ProjectDetailsForm } from "@/components/project/form";
import { ProjectDetailsFormLoader } from "@/components/project/form-loader";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import type { Route } from "./+types/page";
import { GeneralProjectSettingsHeader } from "./header";
import { GeneralProjectSettingsControlSection } from "@/components/project/settings/control-section";
function ProjectSettingsPage({ params }: Route.ComponentProps) {
// router
const { workspaceSlug, projectId } = params;
// store hooks
const { currentProjectDetails } = useProject();
const { allowPermissions } = useUserPermissions();
// derived values
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId);
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined;
return (
<SettingsContentWrapper header={<GeneralProjectSettingsHeader />}>
<PageHead title={pageTitle} />
<div className={`w-full ${isAdmin ? "" : "opacity-60"}`}>
{currentProjectDetails ? (
<ProjectDetailsForm
project={currentProjectDetails}
workspaceSlug={workspaceSlug}
projectId={projectId}
isAdmin={isAdmin}
/>
) : (
<ProjectDetailsFormLoader />
)}
{isAdmin && <GeneralProjectSettingsControlSection projectId={projectId} />}
</div>
</SettingsContentWrapper>
);
}
export default observer(ProjectSettingsPage);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane imports
import { PROJECT_SETTINGS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { SettingsPageHeader } from "@/components/settings/page-header";
import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon";
export const StatesProjectSettingsHeader = observer(function StatesProjectSettingsHeader() {
// translation
const { t } = useTranslation();
// derived values
const settingsDetails = PROJECT_SETTINGS.states;
const Icon = PROJECT_SETTINGS_ICONS.states;
return (
<SettingsPageHeader
leftItem={
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={t(settingsDetails.i18n_label)}
icon={<Icon className="size-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</div>
}
/>
);
});
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// components
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { ProjectStateRoot } from "@/components/project-states";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { SettingsHeading } from "@/components/settings/heading";
// hook
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import type { Route } from "./+types/page";
import { StatesProjectSettingsHeader } from "./header";
function StatesSettingsPage({ params }: Route.ComponentProps) {
const { workspaceSlug, projectId } = params;
// 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 className="h-auto" />;
}
return (
<SettingsContentWrapper header={<StatesProjectSettingsHeader />}>
<PageHead title={pageTitle} />
<div className="w-full">
<SettingsHeading
title={t("project_settings.states.heading")}
description={t("project_settings.states.description")}
/>
<div className="mt-6">
<ProjectStateRoot workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
</div>
</SettingsContentWrapper>
);
}
export default observer(StatesSettingsPage);
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useEffect } from "react";
import { observer } from "mobx-react";
import { Outlet } from "react-router";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useAppRouter } from "@/hooks/use-app-router";
// types
import type { Route } from "./+types/layout";
function ProjectSettingsLayout({ params }: Route.ComponentProps) {
const { workspaceSlug, projectId } = params;
// router
const router = useAppRouter();
// store hooks
const { joinedProjectIds } = useProject();
useEffect(() => {
if (projectId) return;
if (joinedProjectIds.length > 0) {
router.push(`/${workspaceSlug}/settings/projects/${joinedProjectIds[0]}`);
}
}, [joinedProjectIds, router, workspaceSlug, projectId]);
return <Outlet />;
}
export default observer(ProjectSettingsLayout);
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import Link from "next/link";
import { useTheme } from "next-themes";
// plane imports
import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { Button, getButtonStyling } from "@plane/propel/button";
import { cn } from "@plane/utils";
// assets
import ProjectDarkEmptyState from "@/app/assets/empty-state/project-settings/no-projects-dark.png?url";
import ProjectLightEmptyState from "@/app/assets/empty-state/project-settings/no-projects-light.png?url";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
function ProjectSettingsPage() {
// store hooks
const { resolvedTheme } = useTheme();
const { toggleCreateProjectModal } = useCommandPalette();
// derived values
const resolvedPath = resolvedTheme === "dark" ? ProjectDarkEmptyState : ProjectLightEmptyState;
return (
<div className="mx-auto flex h-full max-w-[480px] flex-col items-center justify-center gap-4">
<img src={resolvedPath} alt="No projects yet" />
<div className="text-16 font-semibold text-tertiary">No projects yet</div>
<div className="text-center text-13 text-tertiary">
Projects act as the foundation for goal-driven work. They let you manage your teams, tasks, and everything you
need to get things done.
</div>
<div className="flex gap-2">
<Link href="https://plane.so/" target="_blank" className={cn(getButtonStyling("secondary", "base"))}>
Learn more about projects
</Link>
<Button
onClick={() => toggleCreateProjectModal(true)}
data-ph-element={PROJECT_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_PROJECT_BUTTON}
>
Start your first project
</Button>
</div>
</div>
);
}
export default observer(ProjectSettingsPage);