diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/ai-voice-tasker/page.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/ai-voice-tasker/page.tsx
index 183f86e..30f24ae 100644
--- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/ai-voice-tasker/page.tsx
+++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/ai-voice-tasker/page.tsx
@@ -1,31 +1,12 @@
-/**
- * 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 { PageHead } from "@/components/core/page-title";
-import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
-import { AIVoiceTaskerSettingsContent } from "@/components/workspace/settings/ai-voice-tasker-settings";
-// hooks
-import { useWorkspace } from "@/hooks/store/use-workspace";
+import { redirect } from "react-router";
// local imports
import type { Route } from "./+types/page";
-import { AIVoiceTaskerWorkspaceSettingsHeader } from "./header";
-function AIVoiceTaskerSettingsPage({ params }: Route.ComponentProps) {
+export function clientLoader({ params }: Route.ClientLoaderArgs) {
const { workspaceSlug } = params;
- const { currentWorkspace } = useWorkspace();
- const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - AI / Voice Tasker` : undefined;
-
- return (
- }>
-
-
-
- );
+ throw redirect(`/${workspaceSlug}/?workspaceSettings=ai-voice-tasker`);
}
-export default observer(AIVoiceTaskerSettingsPage);
+export default function AIVoiceTaskerSettingsPage() {
+ return null;
+}
diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/page.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/page.tsx
index cd37a2e..0455966 100644
--- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/page.tsx
+++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/page.tsx
@@ -1,30 +1,12 @@
-/**
- * Copyright (c) 2023-present Plane Software, Inc. and contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- * See the LICENSE file for details.
- */
+import { redirect } from "react-router";
+// local imports
+import type { Route } from "./+types/page";
-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 (
-
-
-
- );
+export function clientLoader({ params }: Route.ClientLoaderArgs) {
+ const { workspaceSlug } = params;
+ throw redirect(`/${workspaceSlug}/?workspaceSettings=general`);
}
-export default observer(BillingSettingsPage);
+export default function BillingSettingsPage() {
+ return null;
+}
diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx
index af6141b..cac8152 100644
--- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx
+++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx
@@ -1,61 +1,12 @@
-/**
- * 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";
+import { redirect } from "react-router";
// local imports
-import { ExportsWorkspaceSettingsHeader } from "./header";
+import type { Route } from "./+types/page";
-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 ;
- }
-
- return (
- } hugging>
-
-
-
-
-
-
- );
+export function clientLoader({ params }: Route.ClientLoaderArgs) {
+ const { workspaceSlug } = params;
+ throw redirect(`/${workspaceSlug}/?workspaceSettings=export`);
}
-export default observer(ExportsPage);
+export default function ExportsPage() {
+ return null;
+}
diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx
index 4366b87..5cf1842 100644
--- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx
+++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx
@@ -1,160 +1,12 @@
-/**
- * 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";
+import { redirect } from "react-router";
// 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("");
- // router
+export function clientLoader({ params }: Route.ClientLoaderArgs) {
const { workspaceSlug } = params;
- // store hooks
- const { workspaceUserInfo, allowPermissions } = useUserPermissions();
- const {
- workspace: { workspaceMemberIds, inviteMembersToWorkspace, filtersStore },
- } = useMember();
- const { currentWorkspace } = useWorkspace();
- const { t } = useTranslation();
+ throw redirect(`/${workspaceSlug}/?workspaceSettings=members`);
+}
- // 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 ;
- }
-
- return (
- } hugging>
-
- setInviteModal(false)}
- onSubmit={handleWorkspaceInvite}
- />
-
-
-
- {t("workspace_settings.settings.members.title")}
- {workspaceMemberIds && workspaceMemberIds.length > 0 && (
-
- )}
-
-
-
-
- setSearchQuery(e.target.value)}
- />
-
-
-
- {canPerformWorkspaceAdminActions && (
-
- )}
-
-
-
-
-
-
- );
-});
-
-export default WorkspaceMembersSettingsPage;
+export default function WorkspaceMembersSettingsPage() {
+ return null;
+}
diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx
index 55422ec..32cb84f 100644
--- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx
+++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx
@@ -1,36 +1,12 @@
-/**
- * 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";
+import { redirect } from "react-router";
// local imports
-import { GeneralWorkspaceSettingsHeader } from "./header";
+import type { Route } from "./+types/page";
-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 (
- }>
-
-
-
- );
+export function clientLoader({ params }: Route.ClientLoaderArgs) {
+ const { workspaceSlug } = params;
+ throw redirect(`/${workspaceSlug}/?workspaceSettings=general`);
}
-export default observer(GeneralWorkspaceSettingsPage);
+export default function GeneralWorkspaceSettingsPage() {
+ return null;
+}
diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx
index 5cb9d3c..6f08186 100644
--- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx
+++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx
@@ -1,111 +1,12 @@
-/**
- * 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";
+import { redirect } from "react-router";
// local imports
import type { Route } from "./+types/page";
-import { WebhookDetailsWorkspaceSettingsHeader } from "./header";
-function WebhookDetailsPage({ params }: Route.ComponentProps) {
- // states
- const [deleteWebhookModal, setDeleteWebhookModal] = useState(false);
- // router
+export function clientLoader({ params }: Route.ClientLoaderArgs) {
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 (
- <>
-
-
-
You are not authorized to access this page.
-
- >
- );
-
- if (!currentWebhook)
- return (
-
-
-
- );
-
- return (
- }>
-
- setDeleteWebhookModal(false)} />
-
-
-
-
- {currentWebhook &&
setDeleteWebhookModal(true)} />}
-
-
- );
+ throw redirect(`/${workspaceSlug}/?workspaceSettings=webhooks&webhookId=${webhookId}`);
}
-export default observer(WebhookDetailsPage);
+export default function WebhookDetailsPage() {
+ return null;
+}
diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx
index c19c5ff..1a37eb7 100644
--- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx
+++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx
@@ -1,109 +1,12 @@
-/**
- * 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 { 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";
-import { WebhooksEmptyState } from "@/components/web-hooks/empty-state";
-// hooks
-import { useWebhook } from "@/hooks/store/use-webhook";
-import { useWorkspace } from "@/hooks/store/use-workspace";
-import { useUserPermissions } from "@/hooks/store/user";
+import { redirect } from "react-router";
// local imports
import type { Route } from "./+types/page";
-import { WebhooksWorkspaceSettingsHeader } from "./header";
-function WebhooksListPage({ params }: Route.ComponentProps) {
- // states
- const [showCreateWebhookModal, setShowCreateWebhookModal] = useState(false);
- // router
+export function clientLoader({ params }: Route.ClientLoaderArgs) {
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 ;
- }
-
- if (!webhooks) return ;
-
- return (
- }>
-
-
-
{
- setShowCreateWebhookModal(false);
- }}
- />
- setShowCreateWebhookModal(true)}
- >
- {t("workspace_settings.settings.webhooks.add_webhook")}
-
- }
- />
- {Object.keys(webhooks).length > 0 ? (
-
-
-
- ) : (
-
-
-
- setShowCreateWebhookModal(true)} />
-
-
-
- )}
-
-
- );
+ throw redirect(`/${workspaceSlug}/?workspaceSettings=webhooks`);
}
-export default observer(WebhooksListPage);
+export default function WebhooksListPage() {
+ return null;
+}
diff --git a/plane-src/apps/web/core/components/web-hooks/webhooks-list-item.tsx b/plane-src/apps/web/core/components/web-hooks/webhooks-list-item.tsx
index 795c05c..0bcd1f7 100644
--- a/plane-src/apps/web/core/components/web-hooks/webhooks-list-item.tsx
+++ b/plane-src/apps/web/core/components/web-hooks/webhooks-list-item.tsx
@@ -9,15 +9,18 @@ import { useParams } from "next/navigation";
// Plane imports
import type { IWebhook } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
+// components
+import { openWorkspaceWebhookSettingsModal } from "@/components/workspace/settings/workspace-settings-modal.utils";
// hooks
import { useWebhook } from "@/hooks/store/use-webhook";
interface IWebhookListItem {
+ useModalLink?: boolean;
webhook: IWebhook;
}
export function WebhooksListItem(props: IWebhookListItem) {
- const { webhook } = props;
+ const { useModalLink = false, webhook } = props;
// router
const { workspaceSlug } = useParams();
// store hooks
@@ -28,17 +31,33 @@ export function WebhooksListItem(props: IWebhookListItem) {
await updateWebhook(workspaceSlug.toString(), webhook.id, { is_active: !webhook.is_active });
};
+ const content = (
+ <>
+ {webhook.url}
+ event.stopPropagation()}>
+
+
+ >
+ );
+
return (
-
-
{webhook.url}
-
-
-
-
+ {useModalLink && webhook.id ? (
+
+ ) : (
+
+ {content}
+
+ )}
);
}
diff --git a/plane-src/apps/web/core/components/web-hooks/webhooks-list.tsx b/plane-src/apps/web/core/components/web-hooks/webhooks-list.tsx
index 5843eee..36ebfaf 100644
--- a/plane-src/apps/web/core/components/web-hooks/webhooks-list.tsx
+++ b/plane-src/apps/web/core/components/web-hooks/webhooks-list.tsx
@@ -10,14 +10,18 @@ import { useWebhook } from "@/hooks/store/use-webhook";
// components
import { WebhooksListItem } from "./webhooks-list-item";
-export const WebhooksList = observer(function WebhooksList() {
+type TWebhooksListProps = {
+ useModalLinks?: boolean;
+};
+
+export const WebhooksList = observer(function WebhooksList({ useModalLinks = false }: TWebhooksListProps) {
// store hooks
const { webhooks } = useWebhook();
return (
{Object.values(webhooks ?? {}).map((webhook) => (
-
+
))}
);
diff --git a/plane-src/apps/web/core/components/workspace/settings/exports-settings.tsx b/plane-src/apps/web/core/components/workspace/settings/exports-settings.tsx
new file mode 100644
index 0000000..d33809f
--- /dev/null
+++ b/plane-src/apps/web/core/components/workspace/settings/exports-settings.tsx
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+// plane imports
+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 { ExportGuide } from "@/components/exporter/guide";
+import { SettingsHeading } from "@/components/settings/heading";
+// hooks
+import { useUserPermissions } from "@/hooks/store/user";
+
+export function WorkspaceExportsSettingsContent() {
+ const { workspaceUserInfo, allowPermissions } = useUserPermissions();
+ const { t } = useTranslation();
+ const canPerformWorkspaceMemberActions = allowPermissions(
+ [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
+ EUserPermissionsLevel.WORKSPACE
+ );
+
+ if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/plane-src/apps/web/core/components/workspace/settings/members-settings.tsx b/plane-src/apps/web/core/components/workspace/settings/members-settings.tsx
new file mode 100644
index 0000000..0459f04
--- /dev/null
+++ b/plane-src/apps/web/core/components/workspace/settings/members-settings.tsx
@@ -0,0 +1,134 @@
+/**
+ * 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";
+// plane imports
+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 { 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 { useUserPermissions } from "@/hooks/store/user";
+// plane web components
+import { BillingActionsButton } from "@/plane-web/components/workspace/billing/billing-actions-button";
+import { MembersActivityButton, SendWorkspaceInvitationModal } from "@/plane-web/components/workspace/members";
+
+type TWorkspaceMembersSettingsContentProps = {
+ workspaceSlug: string;
+};
+
+export function WorkspaceMembersSettingsContent({ workspaceSlug }: TWorkspaceMembersSettingsContentProps) {
+ const [inviteModal, setInviteModal] = useState(false);
+ const [searchQuery, setSearchQuery] = useState("");
+ const { workspaceUserInfo, allowPermissions } = useUserPermissions();
+ const {
+ workspace: { workspaceMemberIds, inviteMembersToWorkspace, filtersStore },
+ } = useMember();
+ const { t } = useTranslation();
+
+ const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
+ const canPerformWorkspaceMemberActions = allowPermissions(
+ [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
+ EUserPermissionsLevel.WORKSPACE
+ );
+ const appliedRoleFilters = filtersStore.filters?.roles || [];
+
+ 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) {
+ const message = error instanceof Error ? (error as Error & { error?: string }).error : undefined;
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Error!",
+ message: `${message ?? t("something_went_wrong_please_try_again")}`,
+ });
+
+ throw error;
+ }
+ };
+
+ const handleRoleFilterUpdate = (role: string) => {
+ const currentRoles = filtersStore.filters?.roles || [];
+ const updatedRoles = currentRoles.includes(role) ? currentRoles.filter((r) => r !== role) : [...currentRoles, role];
+
+ filtersStore.updateFilters({
+ roles: updatedRoles.length > 0 ? updatedRoles : undefined,
+ });
+ };
+
+ if (workspaceUserInfo && !canPerformWorkspaceMemberActions) {
+ return ;
+ }
+
+ return (
+ <>
+ setInviteModal(false)}
+ onSubmit={handleWorkspaceInvite}
+ />
+
+
+
+ {t("workspace_settings.settings.members.title")}
+ {workspaceMemberIds && workspaceMemberIds.length > 0 && (
+
+ )}
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+ {canPerformWorkspaceAdminActions && (
+
+ )}
+
+
+
+
+
+ >
+ );
+}
diff --git a/plane-src/apps/web/core/components/workspace/settings/webhooks-settings.tsx b/plane-src/apps/web/core/components/workspace/settings/webhooks-settings.tsx
new file mode 100644
index 0000000..cbe0f95
--- /dev/null
+++ b/plane-src/apps/web/core/components/workspace/settings/webhooks-settings.tsx
@@ -0,0 +1,173 @@
+/**
+ * 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 useSWR from "swr";
+// plane imports
+import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
+import { useTranslation } from "@plane/i18n";
+import { Button } from "@plane/propel/button";
+import { TOAST_TYPE, setToast } from "@plane/propel/toast";
+import type { IWebhook } from "@plane/types";
+// components
+import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
+import { LogoSpinner } from "@/components/common/logo-spinner";
+import { SettingsHeading } from "@/components/settings/heading";
+import { WebhookSettingsLoader } from "@/components/ui/loader/settings/web-hook";
+import { CreateWebhookModal, DeleteWebhookModal, WebhookDeleteSection, WebhookForm, WebhooksList } from "@/components/web-hooks";
+import { WebhooksEmptyState } from "@/components/web-hooks/empty-state";
+// hooks
+import { useUserPermissions } from "@/hooks/store/user";
+import { useWebhook } from "@/hooks/store/use-webhook";
+import { useWorkspace } from "@/hooks/store/use-workspace";
+
+type TWorkspaceWebhooksSettingsContentProps = {
+ selectedWebhookId?: string;
+ workspaceSlug: string;
+};
+
+export function WorkspaceWebhooksSettingsContent({
+ selectedWebhookId,
+ workspaceSlug,
+}: TWorkspaceWebhooksSettingsContentProps) {
+ const [showCreateWebhookModal, setShowCreateWebhookModal] = useState(false);
+ const { t } = useTranslation();
+ const { workspaceUserInfo, allowPermissions } = useUserPermissions();
+ const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook();
+ const { currentWorkspace } = useWorkspace();
+
+ const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
+
+ useSWR(
+ canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null,
+ canPerformWorkspaceAdminActions ? () => fetchWebhooks(workspaceSlug) : null
+ );
+
+ useEffect(() => {
+ if (!showCreateWebhookModal && webhookSecretKey) clearSecretKey();
+ }, [showCreateWebhookModal, webhookSecretKey, clearSecretKey]);
+
+ if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
+ return ;
+ }
+
+ if (selectedWebhookId) {
+ return ;
+ }
+
+ if (!webhooks) return ;
+
+ return (
+
+
setShowCreateWebhookModal(false)}
+ />
+ setShowCreateWebhookModal(true)}
+ >
+ {t("workspace_settings.settings.webhooks.add_webhook")}
+
+ }
+ />
+ {Object.keys(webhooks).length > 0 ? (
+
+
+
+ ) : (
+
+
+
+ setShowCreateWebhookModal(true)} />
+
+
+
+ )}
+
+ );
+}
+
+type TWorkspaceWebhookDetailsSettingsContentProps = {
+ webhookId: string;
+ workspaceSlug: string;
+};
+
+function WorkspaceWebhookDetailsSettingsContent({ webhookId, workspaceSlug }: TWorkspaceWebhookDetailsSettingsContentProps) {
+ const [deleteWebhookModal, setDeleteWebhookModal] = useState(false);
+ const { currentWebhook, fetchWebhookById, updateWebhook } = useWebhook();
+ const { allowPermissions } = useUserPermissions();
+ const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
+
+ 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 (
+
+
You are not authorized to access this page.
+
+ );
+ }
+
+ if (!currentWebhook)
+ return (
+
+
+
+ );
+
+ return (
+ <>
+ setDeleteWebhookModal(false)} />
+
+
+ {currentWebhook && setDeleteWebhookModal(true)} />}
+
+ >
+ );
+}
diff --git a/plane-src/apps/web/core/components/workspace/settings/workspace-settings-modal.tsx b/plane-src/apps/web/core/components/workspace/settings/workspace-settings-modal.tsx
index 78931bd..937c1a1 100644
--- a/plane-src/apps/web/core/components/workspace/settings/workspace-settings-modal.tsx
+++ b/plane-src/apps/web/core/components/workspace/settings/workspace-settings-modal.tsx
@@ -11,6 +11,7 @@ import { X } from "lucide-react";
import {
EUserPermissionsLevel,
GROUPED_WORKSPACE_SETTINGS,
+ WORKSPACE_SETTINGS,
WORKSPACE_SETTINGS_CATEGORIES,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
@@ -23,7 +24,10 @@ import { SettingsSidebarItem } from "@/components/settings/sidebar/item";
import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon";
import { WorkspaceSettingsSidebarHeader } from "@/components/settings/workspace/sidebar/header";
import { AIVoiceTaskerSettingsContent } from "@/components/workspace/settings/ai-voice-tasker-settings";
+import { WorkspaceExportsSettingsContent } from "@/components/workspace/settings/exports-settings";
+import { WorkspaceMembersSettingsContent } from "@/components/workspace/settings/members-settings";
import { StorageSettingsContent } from "@/components/workspace/settings/storage-settings";
+import { WorkspaceWebhooksSettingsContent } from "@/components/workspace/settings/webhooks-settings";
import { WorkspaceDetails } from "@/components/workspace/settings/workspace-details";
// hooks
import { useUserPermissions } from "@/hooks/store/user";
@@ -32,13 +36,14 @@ import { useWorkspace } from "@/hooks/store/use-workspace";
import {
closeWorkspaceSettingsModal,
getWorkspaceSettingsModalTabFromSearch,
+ getWorkspaceSettingsWebhookIdFromSearch,
openWorkspaceSettingsModal,
WORKSPACE_SETTINGS_MODAL_EVENT,
type TWorkspaceSettingsModalTab,
} from "./workspace-settings-modal.utils";
const HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set(["billing-and-plans"]);
-const MODAL_TABS = new Set(["general", "storage", "ai-voice-tasker"]);
+const MODAL_TABS = new Set(["general", "members", "export", "storage", "webhooks", "ai-voice-tasker"]);
const getInitialTab = (): TWorkspaceSettingsModalTab => {
if (typeof window === "undefined") return "general";
@@ -54,16 +59,21 @@ const getInitialOpenState = () => {
export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal() {
const [activeTab, setActiveTab] = useState(getInitialTab);
+ const [activeWebhookId, setActiveWebhookId] = useState(() =>
+ typeof window === "undefined" ? undefined : getWorkspaceSettingsWebhookIdFromSearch(window.location.search)
+ );
const [isOpen, setIsOpen] = useState(getInitialOpenState);
// store hooks
const { currentWorkspace } = useWorkspace();
const { allowPermissions } = useUserPermissions();
+ const { t } = useTranslation();
useEffect(() => {
const syncFromLocation = () => {
const tab = getWorkspaceSettingsModalTabFromSearch(window.location.search);
setIsOpen(Boolean(tab));
if (tab) setActiveTab(tab);
+ setActiveWebhookId(tab === "webhooks" ? getWorkspaceSettingsWebhookIdFromSearch(window.location.search) : undefined);
};
const handleModalEvent = (event: Event) => {
@@ -71,6 +81,9 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
setIsOpen(detail.isOpen);
if (detail.tab) setActiveTab(detail.tab);
+ setActiveWebhookId(
+ detail.tab === "webhooks" ? getWorkspaceSettingsWebhookIdFromSearch(window.location.search) : undefined
+ );
};
window.addEventListener(WORKSPACE_SETTINGS_MODAL_EVENT, handleModalEvent);
@@ -104,15 +117,28 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
return ;
}
+ if (activeTab === "members" && currentWorkspace?.slug) {
+ return ;
+ }
+
+ if (activeTab === "export") {
+ return ;
+ }
+
if (activeTab === "storage" && currentWorkspace?.slug) {
return ;
}
+ if (activeTab === "webhooks" && currentWorkspace?.slug) {
+ return ;
+ }
+
return ;
};
- const activeTabLabel =
- activeTab === "ai-voice-tasker" ? "AI / Voice Tasker" : activeTab === "storage" ? "хранилище" : "основные параметры";
+ const activeTabLabel = WORKSPACE_SETTINGS[activeTab]?.i18n_label
+ ? t(WORKSPACE_SETTINGS[activeTab].i18n_label)
+ : "основные параметры";
return (
{
const value = new URLSearchParams(search).get(WORKSPACE_SETTINGS_MODAL_QUERY_KEY);
- if (value === "general" || value === "storage" || value === "ai-voice-tasker") return value;
+ if (
+ value === "general" ||
+ value === "members" ||
+ value === "export" ||
+ value === "storage" ||
+ value === "webhooks" ||
+ value === "ai-voice-tasker"
+ )
+ return value;
return undefined;
};
-export const setWorkspaceSettingsModalSearch = (tab: TWorkspaceSettingsModalTab, replace = false) => {
+export const getWorkspaceSettingsWebhookIdFromSearch = (search: string): string | undefined => {
+ const value = new URLSearchParams(search).get(WORKSPACE_SETTINGS_WEBHOOK_QUERY_KEY);
+
+ return value || undefined;
+};
+
+export const setWorkspaceSettingsModalSearch = (
+ tab: TWorkspaceSettingsModalTab,
+ replace = false,
+ options?: { webhookId?: string }
+) => {
if (typeof window === "undefined") return;
const url = new URL(window.location.href);
url.searchParams.set(WORKSPACE_SETTINGS_MODAL_QUERY_KEY, tab);
+ if (tab === "webhooks" && options?.webhookId) {
+ url.searchParams.set(WORKSPACE_SETTINGS_WEBHOOK_QUERY_KEY, options.webhookId);
+ } else {
+ url.searchParams.delete(WORKSPACE_SETTINGS_WEBHOOK_QUERY_KEY);
+ }
window.history[replace ? "replaceState" : "pushState"](window.history.state, "", url);
};
@@ -34,17 +59,26 @@ export const clearWorkspaceSettingsModalSearch = () => {
const url = new URL(window.location.href);
url.searchParams.delete(WORKSPACE_SETTINGS_MODAL_QUERY_KEY);
+ url.searchParams.delete(WORKSPACE_SETTINGS_WEBHOOK_QUERY_KEY);
window.history.replaceState(window.history.state, "", url);
};
-export const openWorkspaceSettingsModal = (tab: TWorkspaceSettingsModalTab = "general", replace = false) => {
+export const openWorkspaceSettingsModal = (
+ tab: TWorkspaceSettingsModalTab = "general",
+ replace = false,
+ options?: { webhookId?: string }
+) => {
if (typeof window === "undefined") return;
- setWorkspaceSettingsModalSearch(tab, replace);
+ setWorkspaceSettingsModalSearch(tab, replace, options);
dispatchWorkspaceSettingsModalEvent({ isOpen: true, tab });
};
+export const openWorkspaceWebhookSettingsModal = (webhookId: string, replace = false) => {
+ openWorkspaceSettingsModal("webhooks", replace, { webhookId });
+};
+
export const closeWorkspaceSettingsModal = () => {
if (typeof window === "undefined") return;