UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: единый модальный режим настроек воркспейса
This commit is contained in:
parent
c3d2d78724
commit
3231ee9b55
|
|
@ -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 (
|
||||
<SettingsContentWrapper header={<AIVoiceTaskerWorkspaceSettingsHeader />}>
|
||||
<PageHead title={pageTitle} />
|
||||
<AIVoiceTaskerSettingsContent workspaceSlug={workspaceSlug} />
|
||||
</SettingsContentWrapper>
|
||||
);
|
||||
throw redirect(`/${workspaceSlug}/?workspaceSettings=ai-voice-tasker`);
|
||||
}
|
||||
|
||||
export default observer(AIVoiceTaskerSettingsPage);
|
||||
export default function AIVoiceTaskerSettingsPage() {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="grid h-full place-items-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
);
|
||||
export function clientLoader({ params }: Route.ClientLoaderArgs) {
|
||||
const { workspaceSlug } = params;
|
||||
throw redirect(`/${workspaceSlug}/?workspaceSettings=general`);
|
||||
}
|
||||
|
||||
export default observer(BillingSettingsPage);
|
||||
export default function BillingSettingsPage() {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <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 function clientLoader({ params }: Route.ClientLoaderArgs) {
|
||||
const { workspaceSlug } = params;
|
||||
throw redirect(`/${workspaceSlug}/?workspaceSettings=export`);
|
||||
}
|
||||
|
||||
export default observer(ExportsPage);
|
||||
export default function ExportsPage() {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string>("");
|
||||
// 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 <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-4">
|
||||
<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="nodedc-settings-field flex min-h-[2.75rem] items-center gap-1.5 px-3.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="ghost"
|
||||
size="lg"
|
||||
className="nodedc-settings-primary-button min-w-[11rem]"
|
||||
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;
|
||||
export default function WorkspaceMembersSettingsPage() {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<SettingsContentWrapper header={<GeneralWorkspaceSettingsHeader />}>
|
||||
<PageHead title={pageTitle} />
|
||||
<WorkspaceDetails />
|
||||
</SettingsContentWrapper>
|
||||
);
|
||||
export function clientLoader({ params }: Route.ClientLoaderArgs) {
|
||||
const { workspaceSlug } = params;
|
||||
throw redirect(`/${workspaceSlug}/?workspaceSettings=general`);
|
||||
}
|
||||
|
||||
export default observer(GeneralWorkspaceSettingsPage);
|
||||
export default function GeneralWorkspaceSettingsPage() {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
throw redirect(`/${workspaceSlug}/?workspaceSettings=webhooks&webhookId=${webhookId}`);
|
||||
}
|
||||
|
||||
export default observer(WebhookDetailsPage);
|
||||
export default function WebhookDetailsPage() {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <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="ghost"
|
||||
size="lg"
|
||||
className="nodedc-settings-primary-button min-w-[11rem]"
|
||||
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">
|
||||
<div className="py-20">
|
||||
<WebhooksEmptyState onClick={() => setShowCreateWebhookModal(true)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingsContentWrapper>
|
||||
);
|
||||
throw redirect(`/${workspaceSlug}/?workspaceSettings=webhooks`);
|
||||
}
|
||||
|
||||
export default observer(WebhooksListPage);
|
||||
export default function WebhooksListPage() {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
<>
|
||||
<h5 className="truncate text-body-sm-medium text-primary">{webhook.url}</h5>
|
||||
<div className="shrink-0" onClick={(event) => event.stopPropagation()}>
|
||||
<ToggleSwitch value={webhook.is_active} onChange={handleToggle} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="nodedc-settings-card px-4 py-3">
|
||||
<Link
|
||||
href={`/${workspaceSlug}/settings/webhooks/${webhook?.id}`}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
<h5 className="truncate text-body-sm-medium text-primary">{webhook.url}</h5>
|
||||
<div className="shrink-0">
|
||||
<ToggleSwitch value={webhook.is_active} onChange={handleToggle} />
|
||||
</div>
|
||||
</Link>
|
||||
{useModalLink && webhook.id ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openWorkspaceWebhookSettingsModal(webhook.id, true)}
|
||||
className="flex w-full items-center justify-between gap-4 text-left"
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${workspaceSlug}/settings/webhooks/${webhook?.id}`}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="nodedc-settings-card flex size-full flex-col gap-y-3 overflow-y-auto p-3">
|
||||
{Object.values(webhooks ?? {}).map((webhook) => (
|
||||
<WebhooksListItem key={webhook.id} webhook={webhook} />
|
||||
<WebhooksListItem key={webhook.id} webhook={webhook} useModalLink={useModalLinks} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<string>("");
|
||||
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 <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SendWorkspaceInvitationModal
|
||||
isOpen={inviteModal}
|
||||
onClose={() => setInviteModal(false)}
|
||||
onSubmit={handleWorkspaceInvite}
|
||||
/>
|
||||
<section
|
||||
className={cn("size-full", {
|
||||
"opacity-60": !canPerformWorkspaceMemberActions,
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 pb-4">
|
||||
<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 flex-wrap items-center gap-2">
|
||||
<div className="nodedc-settings-field flex min-h-[2.75rem] items-center gap-1.5 px-3.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}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<MemberListFiltersDropdown
|
||||
appliedFilters={appliedRoleFilters}
|
||||
handleUpdate={handleRoleFilterUpdate}
|
||||
memberType="workspace"
|
||||
/>
|
||||
<MembersActivityButton workspaceSlug={workspaceSlug} />
|
||||
{canPerformWorkspaceAdminActions && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
className="nodedc-settings-primary-button min-w-[11rem]"
|
||||
onClick={() => setInviteModal(true)}
|
||||
>
|
||||
{t("workspace_settings.settings.members.add_member")}
|
||||
</Button>
|
||||
)}
|
||||
<BillingActionsButton canPerformWorkspaceAdminActions={canPerformWorkspaceAdminActions} />
|
||||
</div>
|
||||
</div>
|
||||
<WorkspaceMembersList searchQuery={searchQuery} isAdmin={canPerformWorkspaceAdminActions} />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
}
|
||||
|
||||
if (selectedWebhookId) {
|
||||
return <WorkspaceWebhookDetailsSettingsContent webhookId={selectedWebhookId} workspaceSlug={workspaceSlug} />;
|
||||
}
|
||||
|
||||
if (!webhooks) return <WebhookSettingsLoader />;
|
||||
|
||||
return (
|
||||
<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="ghost"
|
||||
size="lg"
|
||||
className="nodedc-settings-primary-button min-w-[11rem]"
|
||||
onClick={() => setShowCreateWebhookModal(true)}
|
||||
>
|
||||
{t("workspace_settings.settings.webhooks.add_webhook")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{Object.keys(webhooks).length > 0 ? (
|
||||
<div className="mt-4">
|
||||
<WebhooksList useModalLinks />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="py-20">
|
||||
<WebhooksEmptyState onClick={() => setShowCreateWebhookModal(true)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<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 (
|
||||
<>
|
||||
<DeleteWebhookModal isOpen={deleteWebhookModal} onClose={() => setDeleteWebhookModal(false)} />
|
||||
<div className="w-full space-y-8 overflow-y-auto">
|
||||
<WebhookForm onSubmit={handleUpdateWebhook} data={currentWebhook} />
|
||||
{currentWebhook && <WebhookDeleteSection openDeleteModal={() => setDeleteWebhookModal(true)} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<TWorkspaceSettingsTabs>(["billing-and-plans"]);
|
||||
const MODAL_TABS = new Set<TWorkspaceSettingsTabs>(["general", "storage", "ai-voice-tasker"]);
|
||||
const MODAL_TABS = new Set<TWorkspaceSettingsTabs>(["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<TWorkspaceSettingsModalTab>(getInitialTab);
|
||||
const [activeWebhookId, setActiveWebhookId] = useState<string | undefined>(() =>
|
||||
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 <AIVoiceTaskerSettingsContent workspaceSlug={currentWorkspace.slug} />;
|
||||
}
|
||||
|
||||
if (activeTab === "members" && currentWorkspace?.slug) {
|
||||
return <WorkspaceMembersSettingsContent workspaceSlug={currentWorkspace.slug} />;
|
||||
}
|
||||
|
||||
if (activeTab === "export") {
|
||||
return <WorkspaceExportsSettingsContent />;
|
||||
}
|
||||
|
||||
if (activeTab === "storage" && currentWorkspace?.slug) {
|
||||
return <StorageSettingsContent workspaceSlug={currentWorkspace.slug} />;
|
||||
}
|
||||
|
||||
if (activeTab === "webhooks" && currentWorkspace?.slug) {
|
||||
return <WorkspaceWebhooksSettingsContent selectedWebhookId={activeWebhookId} workspaceSlug={currentWorkspace.slug} />;
|
||||
}
|
||||
|
||||
return <WorkspaceDetails />;
|
||||
};
|
||||
|
||||
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 (
|
||||
<ModalCore
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
export const WORKSPACE_SETTINGS_MODAL_QUERY_KEY = "workspaceSettings";
|
||||
export const WORKSPACE_SETTINGS_MODAL_EVENT = "nodedc:workspace-settings-modal";
|
||||
|
||||
export type TWorkspaceSettingsModalTab = "general" | "storage" | "ai-voice-tasker";
|
||||
export const WORKSPACE_SETTINGS_WEBHOOK_QUERY_KEY = "webhookId";
|
||||
|
||||
export type TWorkspaceSettingsModalTab = "general" | "members" | "export" | "storage" | "webhooks" | "ai-voice-tasker";
|
||||
|
||||
type TWorkspaceSettingsModalEventDetail = {
|
||||
isOpen: boolean;
|
||||
|
|
@ -15,16 +17,39 @@ const dispatchWorkspaceSettingsModalEvent = (detail: TWorkspaceSettingsModalEven
|
|||
export const getWorkspaceSettingsModalTabFromSearch = (search: string): TWorkspaceSettingsModalTab | undefined => {
|
||||
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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue