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;