diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx index 982489d500..142f94ecf5 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/layout.tsx @@ -2,7 +2,6 @@ import { Outlet } from "react-router"; // components import { ContentWrapper } from "@/components/core/content-wrapper"; import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider"; -import { SettingsHeader } from "@/components/settings/header"; export default function SettingsLayout() { return ( @@ -10,10 +9,8 @@ export default function SettingsLayout() {
- {/* Header */} - {/* Content */} - +
diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/header.tsx new file mode 100644 index 0000000000..71e84bf53c --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { WORKSPACE_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; + +export const BillingWorkspaceSettingsHeader = observer(function BillingWorkspaceSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = WORKSPACE_SETTINGS["billing-and-plans"]; + const Icon = WORKSPACE_SETTINGS_ICONS["billing-and-plans"]; + + return ( + + + } + /> + } + /> + +
+ } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/page.tsx index 0ea5d2c9c9..d15d68b1db 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/billing/page.tsx @@ -3,12 +3,14 @@ import { observer } from "mobx-react"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; import { PageHead } from "@/components/core/page-title"; -// hooks import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; +// hooks import { useWorkspace } from "@/hooks/store/use-workspace"; import { useUserPermissions } from "@/hooks/store/user"; // plane web components import { BillingRoot } from "@/plane-web/components/workspace/billing"; +// local imports +import { BillingWorkspaceSettingsHeader } from "./header"; function BillingSettingsPage() { // store hooks @@ -23,7 +25,7 @@ function BillingSettingsPage() { } return ( - + } hugging> diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/header.tsx new file mode 100644 index 0000000000..668f45fa25 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { WORKSPACE_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; + +export const ExportsWorkspaceSettingsHeader = observer(function ExportsWorkspaceSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = WORKSPACE_SETTINGS.export; + const Icon = WORKSPACE_SETTINGS_ICONS.export; + + return ( + + + } + /> + } + /> + + + } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx index 5891e50845..dbe9e2b394 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/exports/page.tsx @@ -1,17 +1,18 @@ import { observer } from "mobx-react"; -// components 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"; -// helpers -// hooks +import { ExportGuide } from "@/components/exporter/guide"; import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import SettingsHeading from "@/components/settings/heading"; +import { SettingsHeading } from "@/components/settings/heading"; +// hooks import { useWorkspace } from "@/hooks/store/use-workspace"; import { useUserPermissions } from "@/hooks/store/user"; +// local imports +import { ExportsWorkspaceSettingsHeader } from "./header"; function ExportsPage() { // store hooks @@ -34,10 +35,10 @@ function ExportsPage() { } return ( - + } hugging>
diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/header.tsx new file mode 100644 index 0000000000..4351be5346 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { WORKSPACE_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; + +export const GeneralWorkspaceSettingsHeader = observer(function GeneralWorkspaceSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = WORKSPACE_SETTINGS.general; + const Icon = WORKSPACE_SETTINGS_ICONS.general; + + return ( + + + } + /> + } + /> + +
+ } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/imports/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/imports/page.tsx deleted file mode 100644 index 89eb978fce..0000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/imports/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { observer } from "mobx-react"; -// components -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import IntegrationGuide from "@/components/integration/guide"; -// hooks -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { SettingsHeading } from "@/components/settings/heading"; -import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUserPermissions } from "@/hooks/store/user"; - -function ImportsPage() { - // router - // store hooks - const { currentWorkspace } = useWorkspace(); - const { allowPermissions } = useUserPermissions(); - // derived values - const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); - const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - Imports` : undefined; - - if (!isAdmin) return ; - - return ( - - -
- - -
-
- ); -} - -export default observer(ImportsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/integrations/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/integrations/page.tsx index 8e4b1b1ea7..91043f17ae 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/integrations/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/integrations/page.tsx @@ -4,8 +4,7 @@ import useSWR from "swr"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; import { PageHead } from "@/components/core/page-title"; -import { SingleIntegrationCard } from "@/components/integration"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; +import { SingleIntegrationCard } from "@/components/integration/single-integration-card"; import { IntegrationAndImportExportBanner } from "@/components/ui/integration-and-import-export-banner"; import { IntegrationsSettingsLoader } from "@/components/ui/loader/settings/integration"; // constants @@ -33,7 +32,7 @@ function WorkspaceIntegrationsPage() { if (!isAdmin) return ; return ( - + <>
@@ -47,7 +46,7 @@ function WorkspaceIntegrationsPage() { )}
-
+ ); } diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx index 9eb72a7829..d392508ee3 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx @@ -4,14 +4,14 @@ import { Outlet } from "react-router"; // components import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; import { getWorkspaceActivePath, pathnameToAccessKey } from "@/components/settings/helper"; -import { SettingsMobileNav } from "@/components/settings/mobile"; +import { SettingsMobileNav } from "@/components/settings/mobile/nav"; // plane imports import { WORKSPACE_SETTINGS_ACCESS } from "@plane/constants"; import type { EUserWorkspaceRoles } from "@plane/types"; +// components +import { WorkspaceSettingsSidebarRoot } from "@/components/settings/workspace/sidebar"; // hooks import { useUserPermissions } from "@/hooks/store/user"; -// local components -import { WorkspaceSettingsSidebar } from "./sidebar"; import type { Route } from "./+types/layout"; @@ -34,18 +34,18 @@ const WorkspaceSettingLayout = observer(function WorkspaceSettingLayout({ params return ( <>
{workspaceUserInfo && !isAuthorized ? ( ) : ( -
-
{}
-
- +
+
+
+
)}
diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/header.tsx new file mode 100644 index 0000000000..72a12a71d2 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { WORKSPACE_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; + +export const MembersWorkspaceSettingsHeader = observer(function MembersWorkspaceSettingsHeader() { + // plane hooks + const { t } = useTranslation(); + // derived values + const settingsDetails = WORKSPACE_SETTINGS.members; + const Icon = WORKSPACE_SETTINGS_ICONS.members; + + return ( + + + } + /> + } + /> + +
+ } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx index 7f8e32fec1..9c869de32a 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx @@ -13,7 +13,6 @@ 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 { SettingsContentWrapper } from "@/components/settings/content-wrapper"; import { WorkspaceMembersList } from "@/components/workspace/settings/members-list"; // hooks import { useMember } from "@/hooks/store/use-member"; @@ -22,7 +21,10 @@ import { useUserPermissions } from "@/hooks/store/user"; // plane web components import { BillingActionsButton } from "@/plane-web/components/workspace/billing/billing-actions-button"; import { SendWorkspaceInvitationModal, MembersActivityButton } from "@/plane-web/components/workspace/members"; +import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; +// local imports import type { Route } from "./+types/page"; +import { MembersWorkspaceSettingsHeader } from "./header"; const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsPage({ params }: Route.ComponentProps) { // states @@ -93,7 +95,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP } return ( - + } hugging>
-

+

{t("workspace_settings.settings.members.title")} {workspaceMemberIds && workspaceMemberIds.length > 0 && ( diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/mobile-header-tabs.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/mobile-header-tabs.tsx deleted file mode 100644 index 822ba83f98..0000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/mobile-header-tabs.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { observer } from "mobx-react"; -import { useParams, usePathname } from "next/navigation"; -import { WORKSPACE_SETTINGS_LINKS, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -// hooks -import { useUserPermissions } from "@/hooks/store/user"; -import { useAppRouter } from "@/hooks/use-app-router"; -// plane web helpers -import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper"; - -export const MobileWorkspaceSettingsTabs = observer(function MobileWorkspaceSettingsTabs() { - const router = useAppRouter(); - const { workspaceSlug } = useParams(); - const pathname = usePathname(); - const { t } = useTranslation(); - // mobx store - const { allowPermissions } = useUserPermissions(); - - return ( -
- {WORKSPACE_SETTINGS_LINKS.map( - (item, index) => - shouldRenderSettingLink(workspaceSlug.toString(), item.key) && - allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) && ( -
router.push(`/${workspaceSlug}${item.href}`)} - > - {t(item.i18n_label)} -
- ) - )} -
- ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx index f7aa43fdea..3025c521b9 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/page.tsx @@ -7,8 +7,10 @@ import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; import { WorkspaceDetails } from "@/components/workspace/settings/workspace-details"; // hooks import { useWorkspace } from "@/hooks/store/use-workspace"; +// local imports +import { GeneralWorkspaceSettingsHeader } from "./header"; -function WorkspaceSettingsPage() { +function GeneralWorkspaceSettingsPage() { // store hooks const { currentWorkspace } = useWorkspace(); const { t } = useTranslation(); @@ -18,11 +20,11 @@ function WorkspaceSettingsPage() { : undefined; return ( - + }> ); } -export default observer(WorkspaceSettingsPage); +export default observer(GeneralWorkspaceSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/sidebar.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/sidebar.tsx deleted file mode 100644 index d4f6aed1a6..0000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/sidebar.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useParams, usePathname } from "next/navigation"; -import { ArrowUpToLine, Building, CreditCard, Users, Webhook } from "lucide-react"; -import type { LucideIcon } from "lucide-react"; -// plane imports -import { - EUserPermissionsLevel, - EUserPermissions, - GROUPED_WORKSPACE_SETTINGS, - WORKSPACE_SETTINGS_CATEGORIES, - WORKSPACE_SETTINGS_CATEGORY, -} from "@plane/constants"; -import type { WORKSPACE_SETTINGS } from "@plane/constants"; -import type { ISvgIcons } from "@plane/propel/icons"; -import type { EUserWorkspaceRoles } from "@plane/types"; -// components -import { SettingsSidebar } from "@/components/settings/sidebar"; -// hooks -import { useUserPermissions } from "@/hooks/store/user"; -// plane web imports -import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper"; - -export const WORKSPACE_SETTINGS_ICONS: Record> = { - general: Building, - members: Users, - export: ArrowUpToLine, - "billing-and-plans": CreditCard, - webhooks: Webhook, -}; - -export function WorkspaceActionIcons({ type, size, className }: { type: string; size?: number; className?: string }) { - if (type === undefined) return null; - const Icon = WORKSPACE_SETTINGS_ICONS[type as keyof typeof WORKSPACE_SETTINGS_ICONS]; - if (!Icon) return null; - return ; -} - -type TWorkspaceSettingsSidebarProps = { - isMobile?: boolean; -}; - -export function WorkspaceSettingsSidebar(props: TWorkspaceSettingsSidebarProps) { - const { isMobile = false } = props; - // router - const pathname = usePathname(); - const { workspaceSlug } = useParams(); // store hooks - const { allowPermissions } = useUserPermissions(); - const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); - - return ( - - isAdmin || ![WORKSPACE_SETTINGS_CATEGORY.FEATURES, WORKSPACE_SETTINGS_CATEGORY.DEVELOPER].includes(category) - )} - groupedSettings={GROUPED_WORKSPACE_SETTINGS} - workspaceSlug={workspaceSlug.toString()} - isActive={(data: { href: string }) => - data.href === "/settings" - ? pathname === `/${workspaceSlug}${data.href}/` - : new RegExp(`^/${workspaceSlug}${data.href}/`).test(pathname) - } - shouldRender={(data: { key: string; access?: EUserWorkspaceRoles[] | undefined }) => - data.access - ? shouldRenderSettingLink(workspaceSlug.toString(), data.key) && - allowPermissions(data.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) - : false - } - actionIcons={WorkspaceActionIcons} - /> - ); -} diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/header.tsx new file mode 100644 index 0000000000..efd2f9fe78 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { WORKSPACE_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; + +export const WebhookDetailsWorkspaceSettingsHeader = observer(function WebhookDetailsWorkspaceSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = WORKSPACE_SETTINGS.webhooks; + const Icon = WORKSPACE_SETTINGS_ICONS.webhooks; + + return ( + + + } + /> + } + /> + +

+ } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx index 441ae6662f..26f506275c 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx @@ -14,7 +14,9 @@ import { DeleteWebhookModal, WebhookDeleteSection, WebhookForm } from "@/compone import { useWebhook } from "@/hooks/store/use-webhook"; import { useWorkspace } from "@/hooks/store/use-workspace"; import { useUserPermissions } from "@/hooks/store/user"; +// local imports import type { Route } from "./+types/page"; +import { WebhookDetailsWorkspaceSettingsHeader } from "./header"; function WebhookDetailsPage({ params }: Route.ComponentProps) { // states @@ -87,7 +89,7 @@ function WebhookDetailsPage({ params }: Route.ComponentProps) { ); return ( - + }> setDeleteWebhookModal(false)} />
diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/header.tsx new file mode 100644 index 0000000000..1136eb2b87 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { WORKSPACE_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; + +export const WebhooksWorkspaceSettingsHeader = observer(function WebhooksWorkspaceSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = WORKSPACE_SETTINGS.webhooks; + const Icon = WORKSPACE_SETTINGS_ICONS.webhooks; + + return ( + + + } + /> + } + /> + +
+ } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx index 41ef5af4f6..096403de22 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/page.tsx @@ -4,19 +4,22 @@ import useSWR from "swr"; // plane imports import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { Button } from "@plane/propel/button"; // components import { EmptyStateCompact } from "@plane/propel/empty-state"; import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; import { PageHead } from "@/components/core/page-title"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; import { SettingsHeading } from "@/components/settings/heading"; import { WebhookSettingsLoader } from "@/components/ui/loader/settings/web-hook"; +import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks"; // hooks import { useWebhook } from "@/hooks/store/use-webhook"; import { useWorkspace } from "@/hooks/store/use-workspace"; import { useUserPermissions } from "@/hooks/store/user"; +// local imports import type { Route } from "./+types/page"; +import { WebhooksWorkspaceSettingsHeader } from "./header"; function WebhooksListPage({ params }: Route.ComponentProps) { // states @@ -53,7 +56,7 @@ function WebhooksListPage({ params }: Route.ComponentProps) { if (!webhooks) return ; return ( - + }>
{ - setShowCreateWebhookModal(true); - }, - }} + control={ + + } /> {Object.keys(webhooks).length > 0 ? ( -
+
) : ( diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx deleted file mode 100644 index c071ef4c35..0000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useState } from "react"; -import { observer } from "mobx-react"; -import useSWR from "swr"; -// plane imports -import { useTranslation } from "@plane/i18n"; -// component -import { EmptyStateCompact } from "@plane/propel/empty-state"; -import { APITokenService } from "@plane/services"; -import { CreateApiTokenModal } from "@/components/api-token/modal/create-token-modal"; -import { ApiTokenListItem } from "@/components/api-token/token-list-item"; -import { PageHead } from "@/components/core/page-title"; -import { SettingsHeading } from "@/components/settings/heading"; -import { APITokenSettingsLoader } from "@/components/ui/loader/settings/api-token"; -import { API_TOKENS_LIST } from "@/constants/fetch-keys"; -// store hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; - -const apiTokenService = new APITokenService(); - -function ApiTokensPage() { - // states - const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false); - // router - // plane hooks - const { t } = useTranslation(); - // store hooks - const { currentWorkspace } = useWorkspace(); - - const { data: tokens } = useSWR(API_TOKENS_LIST, () => apiTokenService.list()); - - const pageTitle = currentWorkspace?.name - ? `${currentWorkspace.name} - ${t("workspace_settings.settings.api_tokens.title")}` - : undefined; - - if (!tokens) { - return ; - } - - return ( -
- - setIsCreateTokenModalOpen(false)} /> -
- {tokens.length > 0 ? ( - <> - { - setIsCreateTokenModalOpen(true); - }, - }} - /> -
- {tokens.map((token) => ( - - ))} -
- - ) : ( -
- { - setIsCreateTokenModalOpen(true); - }, - }} - /> - - { - setIsCreateTokenModalOpen(true); - }, - }, - ]} - align="start" - rootClassName="py-20" - /> -
- )} -
-
- ); -} - -export default observer(ApiTokensPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/layout.tsx deleted file mode 100644 index 25d737300b..0000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/layout.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { observer } from "mobx-react"; -import { usePathname } from "next/navigation"; -import { Outlet } from "react-router"; -// components -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -import { getProfileActivePath } from "@/components/settings/helper"; -import { SettingsMobileNav } from "@/components/settings/mobile"; -// local imports -import { ProfileSidebar } from "./sidebar"; - -function ProfileSettingsLayout() { - // router - const pathname = usePathname(); - - return ( - <> - -
-
- -
-
- - - -
-
- - ); -} - -export default observer(ProfileSettingsLayout); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/preferences/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/preferences/page.tsx deleted file mode 100644 index f3098f675a..0000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/preferences/page.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { observer } from "mobx-react"; -// plane imports -import { useTranslation } from "@plane/i18n"; -// components -import { PageHead } from "@/components/core/page-title"; -import { PreferencesList } from "@/components/preferences/list"; -import { LanguageTimezone } from "@/components/profile/preferences/language-timezone"; -import { ProfileSettingContentHeader } from "@/components/profile/profile-setting-content-header"; -import { SettingsHeading } from "@/components/settings/heading"; -// hooks -import { useUserProfile } from "@/hooks/store/user"; - -const ProfileAppearancePage = observer(() => { - const { t } = useTranslation(); - // hooks - const { data: userProfile } = useUserProfile(); - - if (!userProfile) return <>; - return ( - <> - -
-
- - -
-
- - -
-
- - ); -}); - -export default ProfileAppearancePage; diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/security/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/security/page.tsx deleted file mode 100644 index cd03a7ca01..0000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/security/page.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { useState } from "react"; -import { observer } from "mobx-react"; -import { Controller, useForm } from "react-hook-form"; -import { Eye, EyeOff } from "lucide-react"; -// plane imports -import { E_PASSWORD_STRENGTH } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import { Input, PasswordStrengthIndicator } from "@plane/ui"; -import { getPasswordStrength } from "@plane/utils"; -// components -import { PageHead } from "@/components/core/page-title"; -import { ProfileSettingContentHeader } from "@/components/profile/profile-setting-content-header"; -// helpers -import { authErrorHandler } from "@/helpers/authentication.helper"; -import type { EAuthenticationErrorCodes } from "@/helpers/authentication.helper"; -// hooks -import { useUser } from "@/hooks/store/user"; -// services -import { AuthService } from "@/services/auth.service"; - -export interface FormValues { - old_password: string; - new_password: string; - confirm_password: string; -} - -const defaultValues: FormValues = { - old_password: "", - new_password: "", - confirm_password: "", -}; - -const authService = new AuthService(); - -const defaultShowPassword = { - oldPassword: false, - password: false, - confirmPassword: false, -}; - -function SecurityPage() { - // store - const { data: currentUser, changePassword } = useUser(); - // states - const [showPassword, setShowPassword] = useState(defaultShowPassword); - const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); - const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false); - - // use form - const { - control, - handleSubmit, - watch, - formState: { errors, isSubmitting }, - reset, - } = useForm({ defaultValues }); - // derived values - const oldPassword = watch("old_password"); - const password = watch("new_password"); - const confirmPassword = watch("confirm_password"); - const oldPasswordRequired = !currentUser?.is_password_autoset; - // i18n - const { t } = useTranslation(); - - const isNewPasswordSameAsOldPassword = oldPassword !== "" && password !== "" && password === oldPassword; - - const handleShowPassword = (key: keyof typeof showPassword) => - setShowPassword((prev) => ({ ...prev, [key]: !prev[key] })); - - const handleChangePassword = async (formData: FormValues) => { - const { old_password, new_password } = formData; - try { - const csrfToken = await authService.requestCSRFToken().then((data) => data?.csrf_token); - if (!csrfToken) throw new Error("csrf token not found"); - - await changePassword(csrfToken, { - ...(oldPasswordRequired && { old_password }), - new_password, - }); - - reset(defaultValues); - setShowPassword(defaultShowPassword); - setToast({ - type: TOAST_TYPE.SUCCESS, - title: t("auth.common.password.toast.change_password.success.title"), - message: t("auth.common.password.toast.change_password.success.message"), - }); - } catch (error: unknown) { - let errorInfo = undefined; - if (error instanceof Error) { - const err = error as Error & { error_code?: string }; - const code = err.error_code?.toString(); - errorInfo = code ? authErrorHandler(code as EAuthenticationErrorCodes) : undefined; - } - - setToast({ - type: TOAST_TYPE.ERROR, - title: errorInfo?.title ?? t("auth.common.password.toast.error.title"), - message: - typeof errorInfo?.message === "string" ? errorInfo.message : t("auth.common.password.toast.error.message"), - }); - } - }; - - const isButtonDisabled = - getPasswordStrength(password) != E_PASSWORD_STRENGTH.STRENGTH_VALID || - (oldPasswordRequired && oldPassword.trim() === "") || - password.trim() === "" || - confirmPassword.trim() === "" || - password !== confirmPassword || - password === oldPassword; - - const passwordSupport = password.length > 0 && - getPasswordStrength(password) != E_PASSWORD_STRENGTH.STRENGTH_VALID && ( - - ); - - const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length; - - return ( - <> - - -
-
- {oldPasswordRequired && ( -
-

{t("auth.common.password.current_password.label")}

-
- ( - - )} - /> - {showPassword?.oldPassword ? ( - handleShowPassword("oldPassword")} - /> - ) : ( - handleShowPassword("oldPassword")} - /> - )} -
- {errors.old_password && ( - {errors.old_password.message} - )} -
- )} -
-

{t("auth.common.password.new_password.label")}

-
- ( - setIsPasswordInputFocused(true)} - onBlur={() => setIsPasswordInputFocused(false)} - /> - )} - /> - {showPassword?.password ? ( - handleShowPassword("password")} - /> - ) : ( - handleShowPassword("password")} - /> - )} -
- {passwordSupport} - {isNewPasswordSameAsOldPassword && !isPasswordInputFocused && ( - - {t("new_password_must_be_different_from_old_password")} - - )} -
-
-

{t("auth.common.password.confirm_password.label")}

-
- ( - setIsRetryPasswordInputFocused(true)} - onBlur={() => setIsRetryPasswordInputFocused(false)} - /> - )} - /> - {showPassword?.confirmPassword ? ( - handleShowPassword("confirmPassword")} - /> - ) : ( - handleShowPassword("confirmPassword")} - /> - )} -
- {!!confirmPassword && password !== confirmPassword && renderPasswordMatchError && ( - {t("auth.common.password.errors.match")} - )} -
-
- -
- -
-
- - ); -} - -export default observer(SecurityPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx deleted file mode 100644 index 4088ec4ab3..0000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { observer } from "mobx-react"; -import { useParams, usePathname } from "next/navigation"; -import { CircleUser, Activity, Bell, CircleUserRound, KeyRound, Settings2, Blocks } from "lucide-react"; -// plane imports -import { GROUPED_PROFILE_SETTINGS, PROFILE_SETTINGS_CATEGORIES } from "@plane/constants"; -import { LockIcon } from "@plane/propel/icons"; -import { getFileURL } from "@plane/utils"; -// components -import { SettingsSidebar } from "@/components/settings/sidebar"; -// hooks -import { useUser } from "@/hooks/store/user"; - -const ICONS = { - profile: CircleUser, - security: LockIcon, - activity: Activity, - preferences: Settings2, - notifications: Bell, - "api-tokens": KeyRound, - connections: Blocks, -}; - -export function ProjectActionIcons({ type, size, className }: { type: string; size?: number; className?: string }) { - if (type === undefined) return null; - const Icon = ICONS[type as keyof typeof ICONS]; - if (!Icon) return null; - return ; -} - -type TProfileSidebarProps = { - isMobile?: boolean; -}; - -export const ProfileSidebar = observer(function ProfileSidebar(props: TProfileSidebarProps) { - const { isMobile = false } = props; - // router - const pathname = usePathname(); - const { workspaceSlug } = useParams(); - // store hooks - const { data: currentUser } = useUser(); - - return ( - pathname === `/${workspaceSlug}${data.href}/`} - customHeader={ -
-
- {!currentUser?.avatar_url || currentUser?.avatar_url === "" ? ( -
- -
- ) : ( -
- {currentUser?.display_name} -
- )} -
-
-
{currentUser?.display_name}
-
{currentUser?.email}
-
-
- } - actionIcons={ProjectActionIcons} - shouldRender - /> - ); -}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/header.tsx new file mode 100644 index 0000000000..e736d892bd --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { PROJECT_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; + +export const AutomationsProjectSettingsHeader = observer(function AutomationsProjectSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = PROJECT_SETTINGS.automations; + const Icon = PROJECT_SETTINGS_ICONS.automations; + + return ( + + + } + /> + } + /> + +
+ } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx index 3607757553..657a78c961 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx @@ -1,21 +1,22 @@ import { observer } from "mobx-react"; +// plane imports import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { IProject } from "@plane/types"; -// ui -// components import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; import { AutoArchiveAutomation, AutoCloseAutomation } from "@/components/automation"; import { PageHead } from "@/components/core/page-title"; -// hooks import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; import { SettingsHeading } from "@/components/settings/heading"; +// hooks import { useProject } from "@/hooks/store/use-project"; import { useUserPermissions } from "@/hooks/store/user"; // plane web imports import { CustomAutomationsRoot } from "@/plane-web/components/automations/root"; +// local imports import type { Route } from "./+types/page"; +import { AutomationsProjectSettingsHeader } from "./header"; function AutomationSettingsPage({ params }: Route.ComponentProps) { // router @@ -51,15 +52,17 @@ function AutomationSettingsPage({ params }: Route.ComponentProps) { } return ( - + } hugging>
- - +
+ + +
diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/header.tsx new file mode 100644 index 0000000000..34086c6b06 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { PROJECT_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; + +export const EstimatesProjectSettingsHeader = observer(function EstimatesProjectSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = PROJECT_SETTINGS.estimates; + const Icon = PROJECT_SETTINGS_ICONS.estimates; + + return ( + + + } + /> + } + /> + +
+ } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx index 2d446ae7ca..44fc12c7d4 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/estimates/page.tsx @@ -4,11 +4,13 @@ import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; import { PageHead } from "@/components/core/page-title"; import { EstimateRoot } from "@/components/estimates"; -// hooks import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; +// hooks import { useProject } from "@/hooks/store/use-project"; import { useUserPermissions } from "@/hooks/store/user"; +// local imports import type { Route } from "./+types/page"; +import { EstimatesProjectSettingsHeader } from "./header"; function EstimatesSettingsPage({ params }: Route.ComponentProps) { const { workspaceSlug, projectId } = params; @@ -25,7 +27,7 @@ function EstimatesSettingsPage({ params }: Route.ComponentProps) { } return ( - + }>
diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/header.tsx new file mode 100644 index 0000000000..756eaac160 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { PROJECT_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; + +export const FeaturesCyclesProjectSettingsHeader = observer(function FeaturesCyclesProjectSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = PROJECT_SETTINGS.features_cycles; + const Icon = PROJECT_SETTINGS_ICONS.features_cycles; + + return ( + + + } + /> + } + /> + +
+ } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/page.tsx new file mode 100644 index 0000000000..a943c15096 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/page.tsx @@ -0,0 +1,58 @@ +import { observer } from "mobx-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// components +import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; +import { PageHead } from "@/components/core/page-title"; +import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item"; +import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; +// hooks +import { useProject } from "@/hooks/store/use-project"; +import { useUserPermissions } from "@/hooks/store/user"; +// local imports +import type { Route } from "./+types/page"; +import { FeaturesCyclesProjectSettingsHeader } from "./header"; +import { SettingsHeading } from "@/components/settings/heading"; + +function FeaturesCyclesSettingsPage({ params }: Route.ComponentProps) { + const { workspaceSlug, projectId } = params; + // store hooks + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); + const { currentProjectDetails } = useProject(); + // translation + const { t } = useTranslation(); + // derived values + const pageTitle = currentProjectDetails?.name + ? `${currentProjectDetails?.name} settings - ${t("project_settings.features.cycles.short_title")}` + : undefined; + const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); + + if (workspaceUserInfo && !canPerformProjectAdminActions) { + return ; + } + + return ( + }> + +
+ +
+ +
+
+
+ ); +} + +export default observer(FeaturesCyclesSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/header.tsx new file mode 100644 index 0000000000..0bbff98579 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { PROJECT_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; + +export const FeaturesIntakeProjectSettingsHeader = observer(function FeaturesIntakeProjectSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = PROJECT_SETTINGS.features_intake; + const Icon = PROJECT_SETTINGS_ICONS.features_intake; + + return ( + + + } + /> + } + /> + +
+ } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/page.tsx new file mode 100644 index 0000000000..a8ac0adebf --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/page.tsx @@ -0,0 +1,58 @@ +import { observer } from "mobx-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// components +import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; +import { PageHead } from "@/components/core/page-title"; +import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; +import { SettingsHeading } from "@/components/settings/heading"; +import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item"; +// hooks +import { useProject } from "@/hooks/store/use-project"; +import { useUserPermissions } from "@/hooks/store/user"; +// local imports +import type { Route } from "./+types/page"; +import { FeaturesIntakeProjectSettingsHeader } from "./header"; + +function FeaturesIntakeSettingsPage({ params }: Route.ComponentProps) { + const { workspaceSlug, projectId } = params; + // store hooks + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); + const { currentProjectDetails } = useProject(); + // translation + const { t } = useTranslation(); + // derived values + const pageTitle = currentProjectDetails?.name + ? `${currentProjectDetails?.name} settings - ${t("project_settings.features.intake.short_title")}` + : undefined; + const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); + + if (workspaceUserInfo && !canPerformProjectAdminActions) { + return ; + } + + return ( + }> + +
+ +
+ +
+
+
+ ); +} + +export default observer(FeaturesIntakeSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/header.tsx new file mode 100644 index 0000000000..397a2b6a48 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { PROJECT_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; + +export const FeaturesModulesProjectSettingsHeader = observer(function FeaturesModulesProjectSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = PROJECT_SETTINGS.features_modules; + const Icon = PROJECT_SETTINGS_ICONS.features_modules; + + return ( + + + } + /> + } + /> + + + } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/page.tsx new file mode 100644 index 0000000000..7f5f540fe7 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/page.tsx @@ -0,0 +1,58 @@ +import { observer } from "mobx-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// components +import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; +import { PageHead } from "@/components/core/page-title"; +import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; +import { SettingsHeading } from "@/components/settings/heading"; +import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item"; +// hooks +import { useProject } from "@/hooks/store/use-project"; +import { useUserPermissions } from "@/hooks/store/user"; +// local imports +import type { Route } from "./+types/page"; +import { FeaturesModulesProjectSettingsHeader } from "./header"; + +function FeaturesModulesSettingsPage({ params }: Route.ComponentProps) { + const { workspaceSlug, projectId } = params; + // store hooks + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); + const { currentProjectDetails } = useProject(); + // translation + const { t } = useTranslation(); + // derived values + const pageTitle = currentProjectDetails?.name + ? `${currentProjectDetails?.name} settings - ${t("project_settings.features.modules.short_title")}` + : undefined; + const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); + + if (workspaceUserInfo && !canPerformProjectAdminActions) { + return ; + } + + return ( + }> + +
+ +
+ +
+
+
+ ); +} + +export default observer(FeaturesModulesSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx deleted file mode 100644 index 6db9c4bd09..0000000000 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { observer } from "mobx-react"; -// components -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; -import { PageHead } from "@/components/core/page-title"; -import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; -// hooks -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions } from "@/hooks/store/user"; -import { ProjectFeaturesList } from "@/plane-web/components/projects/settings/features-list"; -import type { Route } from "./+types/page"; - -function FeaturesSettingsPage({ params }: Route.ComponentProps) { - const { workspaceSlug, projectId } = params; - // store - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - - const { currentProjectDetails } = useProject(); - // derived values - const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Features` : undefined; - const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); - - if (workspaceUserInfo && !canPerformProjectAdminActions) { - return ; - } - - return ( - - -
- -
-
- ); -} - -export default observer(FeaturesSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/header.tsx new file mode 100644 index 0000000000..9ee3db684f --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { PROJECT_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; + +export const FeaturesPagesProjectSettingsHeader = observer(function FeaturesPagesProjectSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = PROJECT_SETTINGS.features_pages; + const Icon = PROJECT_SETTINGS_ICONS.features_pages; + + return ( + + + } + /> + } + /> + + + } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/page.tsx new file mode 100644 index 0000000000..05ed1e6cb0 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/page.tsx @@ -0,0 +1,58 @@ +import { observer } from "mobx-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// components +import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; +import { PageHead } from "@/components/core/page-title"; +import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; +import { SettingsHeading } from "@/components/settings/heading"; +import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item"; +// hooks +import { useProject } from "@/hooks/store/use-project"; +import { useUserPermissions } from "@/hooks/store/user"; +// local imports +import type { Route } from "./+types/page"; +import { FeaturesPagesProjectSettingsHeader } from "./header"; + +function FeaturesPagesSettingsPage({ params }: Route.ComponentProps) { + const { workspaceSlug, projectId } = params; + // store hooks + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); + const { currentProjectDetails } = useProject(); + // translation + const { t } = useTranslation(); + // derived values + const pageTitle = currentProjectDetails?.name + ? `${currentProjectDetails?.name} settings - ${t("project_settings.features.pages.short_title")}` + : undefined; + const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); + + if (workspaceUserInfo && !canPerformProjectAdminActions) { + return ; + } + + return ( + }> + +
+ +
+ +
+
+
+ ); +} + +export default observer(FeaturesPagesSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/header.tsx new file mode 100644 index 0000000000..4ca18074a5 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { PROJECT_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; + +export const FeaturesViewsProjectSettingsHeader = observer(function FeaturesViewsProjectSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = PROJECT_SETTINGS.features_views; + const Icon = PROJECT_SETTINGS_ICONS.features_views; + + return ( + + + } + /> + } + /> + + + } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/page.tsx new file mode 100644 index 0000000000..39f46d11f2 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/page.tsx @@ -0,0 +1,58 @@ +import { observer } from "mobx-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// components +import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; +import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; +import { PageHead } from "@/components/core/page-title"; +import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; +import { SettingsHeading } from "@/components/settings/heading"; +import { ProjectSettingsFeatureControlItem } from "@/components/settings/project/content/feature-control-item"; +// hooks +import { useProject } from "@/hooks/store/use-project"; +import { useUserPermissions } from "@/hooks/store/user"; +// local imports +import type { Route } from "./+types/page"; +import { FeaturesViewsProjectSettingsHeader } from "./header"; + +function FeaturesViewsSettingsPage({ params }: Route.ComponentProps) { + const { workspaceSlug, projectId } = params; + // store hooks + const { workspaceUserInfo, allowPermissions } = useUserPermissions(); + const { currentProjectDetails } = useProject(); + // translation + const { t } = useTranslation(); + // derived values + const pageTitle = currentProjectDetails?.name + ? `${currentProjectDetails?.name} settings - ${t("project_settings.features.views.short_title")}` + : undefined; + const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); + + if (workspaceUserInfo && !canPerformProjectAdminActions) { + return ; + } + + return ( + }> + +
+ +
+ +
+
+
+ ); +} + +export default observer(FeaturesViewsSettingsPage); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/header.tsx new file mode 100644 index 0000000000..ba3e7c5fd1 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { PROJECT_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; + +export const GeneralProjectSettingsHeader = observer(function GeneralProjectSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = PROJECT_SETTINGS.general; + const Icon = PROJECT_SETTINGS_ICONS.general; + + return ( + + + } + /> + } + /> + + + } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/header.tsx new file mode 100644 index 0000000000..0138b0ea84 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { PROJECT_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; + +export const LabelsProjectSettingsHeader = observer(function LabelsProjectSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = PROJECT_SETTINGS.labels; + const Icon = PROJECT_SETTINGS_ICONS.labels; + + return ( + + + } + /> + } + /> + + + } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx index 812b583023..5b2416f9ec 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/labels/page.tsx @@ -7,10 +7,12 @@ import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; import { PageHead } from "@/components/core/page-title"; import { ProjectSettingsLabelList } from "@/components/labels"; -// hooks import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; +// hooks import { useProject } from "@/hooks/store/use-project"; import { useUserPermissions } from "@/hooks/store/user"; +// local imports +import { LabelsProjectSettingsHeader } from "./header"; function LabelsSettingsPage() { // store hooks @@ -45,9 +47,9 @@ function LabelsSettingsPage() { } return ( - + }> -
+
diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/layout.tsx index 8e8c09064d..8381edbe3d 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/layout.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/layout.tsx @@ -3,12 +3,12 @@ import { usePathname } from "next/navigation"; import { Outlet } from "react-router"; // components import { getProjectActivePath } from "@/components/settings/helper"; -import { SettingsMobileNav } from "@/components/settings/mobile"; -import { ProjectSettingsSidebar } from "@/components/settings/project/sidebar"; +import { SettingsMobileNav } from "@/components/settings/mobile/nav"; // plane web imports import { ProjectAuthWrapper } from "@/plane-web/layouts/project-wrapper"; // types import type { Route } from "./+types/layout"; +import { ProjectSettingsSidebarRoot } from "@/components/settings/project/sidebar"; function ProjectDetailSettingsLayout({ params }: Route.ComponentProps) { const { workspaceSlug, projectId } = params; @@ -17,14 +17,19 @@ function ProjectDetailSettingsLayout({ params }: Route.ComponentProps) { return ( <> - -
-
{projectId && }
- -
- + } + activePath={getProjectActivePath(pathname) || ""} + /> +
+
+
+
- + + + +
); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/header.tsx new file mode 100644 index 0000000000..c9a2348fb0 --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { PROJECT_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; + +export const MembersProjectSettingsHeader = observer(function MembersProjectSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = PROJECT_SETTINGS.members; + const Icon = PROJECT_SETTINGS_ICONS.members; + + return ( + + + } + /> + } + /> + +
+ } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx index 3bdf8e9991..21141c14ca 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/members/page.tsx @@ -5,17 +5,19 @@ import { useTranslation } from "@plane/i18n"; // components import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; import { PageHead } from "@/components/core/page-title"; -// hooks import { ProjectMemberList } from "@/components/project/member-list"; import { ProjectSettingsMemberDefaults } from "@/components/project/project-settings-member-defaults"; import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; import { SettingsHeading } from "@/components/settings/heading"; +// hooks import { useProject } from "@/hooks/store/use-project"; import { useUserPermissions } from "@/hooks/store/user"; // plane web imports import { ProjectTeamspaceList } from "@/plane-web/components/projects/teamspaces/teamspace-list"; import { getProjectSettingsPageLabelI18nKey } from "@/plane-web/helpers/project-settings"; +// local imports import type { Route } from "./+types/page"; +import { MembersProjectSettingsHeader } from "./header"; function MembersSettingsPage({ params }: Route.ComponentProps) { // router @@ -39,7 +41,7 @@ function MembersSettingsPage({ params }: Route.ComponentProps) { } return ( - + } hugging> diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx index ea0486fa37..00b9d7b534 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/page.tsx @@ -1,25 +1,20 @@ -import { useState } from "react"; import { observer } from "mobx-react"; // plane imports import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; // components import { PageHead } from "@/components/core/page-title"; -import { DeleteProjectModal } from "@/components/project/delete-project-modal"; import { ProjectDetailsForm } from "@/components/project/form"; import { ProjectDetailsFormLoader } from "@/components/project/form-loader"; -import { ArchiveRestoreProjectModal } from "@/components/project/settings/archive-project/archive-restore-modal"; -import { ArchiveProjectSelection } from "@/components/project/settings/archive-project/selection"; -import { DeleteProjectSection } from "@/components/project/settings/delete-project-section"; import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; // hooks import { useProject } from "@/hooks/store/use-project"; import { useUserPermissions } from "@/hooks/store/user"; +// local imports import type { Route } from "./+types/page"; +import { GeneralProjectSettingsHeader } from "./header"; +import { GeneralProjectSettingsControlSection } from "@/components/project/settings/control-section"; function ProjectSettingsPage({ params }: Route.ComponentProps) { - // states - const [selectProject, setSelectedProject] = useState(null); - const [archiveProject, setArchiveProject] = useState(false); // router const { workspaceSlug, projectId } = params; // store hooks @@ -31,25 +26,8 @@ function ProjectSettingsPage({ params }: Route.ComponentProps) { const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - General Settings` : undefined; return ( - + }> - {currentProjectDetails && ( - <> - setArchiveProject(false)} - archive - /> - setSelectedProject(null)} - /> - - )} -
{currentProjectDetails ? ( )} - - {isAdmin && currentProjectDetails && ( - <> - setArchiveProject(true)} - /> - setSelectedProject(currentProjectDetails.id ?? null)} - /> - - )} + {isAdmin && }
); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/header.tsx new file mode 100644 index 0000000000..f69d22922b --- /dev/null +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/header.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { PROJECT_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; +import { SettingsPageHeader } from "@/components/settings/page-header"; +import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; + +export const StatesProjectSettingsHeader = observer(function StatesProjectSettingsHeader() { + // translation + const { t } = useTranslation(); + // derived values + const settingsDetails = PROJECT_SETTINGS.states; + const Icon = PROJECT_SETTINGS_ICONS.states; + + return ( + + + } + /> + } + /> + +
+ } + /> + ); +}); diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx index 31d8ccbc1a..ff3309d458 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/states/page.tsx @@ -5,12 +5,14 @@ import { useTranslation } from "@plane/i18n"; import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; import { PageHead } from "@/components/core/page-title"; import { ProjectStateRoot } from "@/components/project-states"; -// hook import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; import { SettingsHeading } from "@/components/settings/heading"; +// hook import { useProject } from "@/hooks/store/use-project"; import { useUserPermissions } from "@/hooks/store/user"; +// local imports import type { Route } from "./+types/page"; +import { StatesProjectSettingsHeader } from "./header"; function StatesSettingsPage({ params }: Route.ComponentProps) { const { workspaceSlug, projectId } = params; @@ -33,14 +35,16 @@ function StatesSettingsPage({ params }: Route.ComponentProps) { } return ( - + }>
- +
+ +
); diff --git a/apps/web/app/(all)/[workspaceSlug]/layout.tsx b/apps/web/app/(all)/[workspaceSlug]/layout.tsx index 0e489644d4..854e6bbaf2 100644 --- a/apps/web/app/(all)/[workspaceSlug]/layout.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/layout.tsx @@ -2,14 +2,19 @@ import { Outlet } from "react-router"; import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; import { WorkspaceContentWrapper } from "@/plane-web/components/workspace/content-wrapper"; import { AppRailVisibilityProvider } from "@/plane-web/hooks/app-rail"; +import { GlobalModals } from "@/plane-web/components/common/modal/global"; import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper"; +import type { Route } from "./+types/layout"; + +export default function WorkspaceLayout(props: Route.ComponentProps) { + const { workspaceSlug } = props.params; -export default function WorkspaceLayout() { return ( + diff --git a/apps/web/app/(all)/profile/activity/page.tsx b/apps/web/app/(all)/profile/activity/page.tsx deleted file mode 100644 index e3956258ff..0000000000 --- a/apps/web/app/(all)/profile/activity/page.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useState } from "react"; -import { observer } from "mobx-react"; -import { useTheme } from "next-themes"; -// plane imports -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -// assets -import darkActivityAsset from "@/app/assets/empty-state/profile/activity-dark.webp?url"; -import lightActivityAsset from "@/app/assets/empty-state/profile/activity-light.webp?url"; -// components -import { PageHead } from "@/components/core/page-title"; -import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; -import { ProfileActivityListPage } from "@/components/profile/activity/profile-activity-list"; -import { ProfileSettingContentHeader } from "@/components/profile/profile-setting-content-header"; -import { ProfileSettingContentWrapper } from "@/components/profile/profile-setting-content-wrapper"; - -const PER_PAGE = 100; - -function ProfileActivityPage() { - // states - const [pageCount, setPageCount] = useState(1); - const [totalPages, setTotalPages] = useState(0); - const [resultsCount, setResultsCount] = useState(0); - const [isEmpty, setIsEmpty] = useState(false); - // theme hook - const { resolvedTheme } = useTheme(); - // plane hooks - const { t } = useTranslation(); - // derived values - const resolvedPath = resolvedTheme === "light" ? lightActivityAsset : darkActivityAsset; - - const updateTotalPages = (count: number) => setTotalPages(count); - - const updateResultsCount = (count: number) => setResultsCount(count); - - const updateEmptyState = (isEmpty: boolean) => setIsEmpty(isEmpty); - - const handleLoadMore = () => setPageCount((prev) => prev + 1); - - const activityPages: React.ReactNode[] = []; - for (let i = 0; i < pageCount; i++) - activityPages.push( - - ); - - const isLoadMoreVisible = pageCount < totalPages && resultsCount !== 0; - - if (isEmpty) { - return ( - - ); - } - - return ( - <> - - - - {activityPages} - {isLoadMoreVisible && ( -
- -
- )} -
- - ); -} - -export default observer(ProfileActivityPage); diff --git a/apps/web/app/(all)/profile/appearance/page.tsx b/apps/web/app/(all)/profile/appearance/page.tsx deleted file mode 100644 index d0d05588d8..0000000000 --- a/apps/web/app/(all)/profile/appearance/page.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useCallback, useMemo } from "react"; -import { observer } from "mobx-react"; -import { useTheme } from "next-themes"; -// plane imports -import type { I_THEME_OPTION } from "@plane/constants"; -import { THEME_OPTIONS } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { setPromiseToast } from "@plane/propel/toast"; -import { applyCustomTheme } from "@plane/utils"; -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; -import { PageHead } from "@/components/core/page-title"; -import { CustomThemeSelector } from "@/components/core/theme/custom-theme-selector"; -import { ThemeSwitch } from "@/components/core/theme/theme-switch"; -import { ProfileSettingContentHeader } from "@/components/profile/profile-setting-content-header"; -import { ProfileSettingContentWrapper } from "@/components/profile/profile-setting-content-wrapper"; -// hooks -import { useUserProfile } from "@/hooks/store/user"; - -function ProfileAppearancePage() { - // store hooks - const { data: userProfile, updateUserTheme } = useUserProfile(); - // theme - const { setTheme } = useTheme(); - // translation - const { t } = useTranslation(); - // derived values - const currentTheme = useMemo(() => { - const userThemeOption = THEME_OPTIONS.find((t) => t.value === userProfile?.theme?.theme); - return userThemeOption || null; - }, [userProfile?.theme?.theme]); - - const handleThemeChange = useCallback( - async (themeOption: I_THEME_OPTION) => { - setTheme(themeOption.value); - - // If switching to custom theme and user has saved custom colors, apply them immediately - if ( - themeOption.value === "custom" && - userProfile?.theme?.primary && - userProfile?.theme?.background && - userProfile?.theme?.darkPalette !== undefined - ) { - applyCustomTheme( - userProfile.theme.primary, - userProfile.theme.background, - userProfile.theme.darkPalette ? "dark" : "light" - ); - } - - const updateCurrentUserThemePromise = updateUserTheme({ theme: themeOption.value }); - setPromiseToast(updateCurrentUserThemePromise, { - loading: "Updating theme...", - success: { - title: "Theme updated", - message: () => "Reloading to apply changes...", - }, - error: { - title: "Error!", - message: () => "Failed to update theme. Please try again.", - }, - }); - // Wait for the promise to resolve, then reload after showing toast - try { - await updateCurrentUserThemePromise; - window.location.reload(); - } catch (error) { - // Error toast already shown by setPromiseToast - console.error("Error updating theme:", error); - } - }, - [setTheme, updateUserTheme, userProfile] - ); - - return ( - <> - - {userProfile ? ( - - -
-
-

{t("theme")}

-

{t("select_or_customize_your_interface_color_scheme")}

-
-
- -
-
- {userProfile?.theme?.theme === "custom" && } -
- ) : ( -
- -
- )} - - ); -} - -export default observer(ProfileAppearancePage); diff --git a/apps/web/app/(all)/profile/notifications/page.tsx b/apps/web/app/(all)/profile/notifications/page.tsx deleted file mode 100644 index 725117e5c2..0000000000 --- a/apps/web/app/(all)/profile/notifications/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import useSWR from "swr"; -// components -import { useTranslation } from "@plane/i18n"; -import { PageHead } from "@/components/core/page-title"; -import { EmailNotificationForm } from "@/components/profile/notification/email-notification-form"; -import { ProfileSettingContentHeader } from "@/components/profile/profile-setting-content-header"; -import { ProfileSettingContentWrapper } from "@/components/profile/profile-setting-content-wrapper"; -import { EmailSettingsLoader } from "@/components/ui/loader/settings/email"; -// services -import { UserService } from "@/services/user.service"; - -const userService = new UserService(); - -export default function ProfileNotificationPage() { - const { t } = useTranslation(); - // fetching user email notification settings - const { data, isLoading } = useSWR("CURRENT_USER_EMAIL_NOTIFICATION_SETTINGS", () => - userService.currentUserEmailNotificationSettings() - ); - - if (!data || isLoading) { - return ; - } - - return ( - <> - - - - - - - ); -} diff --git a/apps/web/app/(all)/profile/page.tsx b/apps/web/app/(all)/profile/page.tsx deleted file mode 100644 index 9b6f8f083c..0000000000 --- a/apps/web/app/(all)/profile/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { observer } from "mobx-react"; -// plane imports -import { useTranslation } from "@plane/i18n"; -// components -import { LogoSpinner } from "@/components/common/logo-spinner"; -import { PageHead } from "@/components/core/page-title"; -import { ProfileForm } from "@/components/profile/form"; -import { ProfileSettingContentWrapper } from "@/components/profile/profile-setting-content-wrapper"; -// hooks -import { useUser } from "@/hooks/store/user"; - -function ProfileSettingsPage() { - const { t } = useTranslation(); - // store hooks - const { data: currentUser, userProfile } = useUser(); - - if (!currentUser) - return ( -
- -
- ); - - return ( - <> - - - - - - ); -} - -export default observer(ProfileSettingsPage); diff --git a/apps/web/app/(all)/profile/sidebar.tsx b/apps/web/app/(all)/profile/sidebar.tsx deleted file mode 100644 index c4ded1df24..0000000000 --- a/apps/web/app/(all)/profile/sidebar.tsx +++ /dev/null @@ -1,279 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; -// icons -import { LogOut, MoveLeft, Activity, Bell, CircleUser, KeyRound, Settings2, CirclePlus, Mails } from "lucide-react"; -// plane imports -import { PROFILE_ACTION_LINKS } from "@plane/constants"; -import { useOutsideClickDetector } from "@plane/hooks"; -import { useTranslation } from "@plane/i18n"; -import { ChevronLeftIcon } from "@plane/propel/icons"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import { Tooltip } from "@plane/propel/tooltip"; -import { cn, getFileURL } from "@plane/utils"; -// components -import { SidebarNavItem } from "@/components/sidebar/sidebar-navigation"; -// hooks -import { useAppTheme } from "@/hooks/store/use-app-theme"; -import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUser, useUserSettings } from "@/hooks/store/user"; -import { usePlatformOS } from "@/hooks/use-platform-os"; - -const WORKSPACE_ACTION_LINKS = [ - { - key: "create_workspace", - Icon: CirclePlus, - i18n_label: "create_workspace", - href: "/create-workspace", - }, - { - key: "invitations", - Icon: Mails, - i18n_label: "workspace_invites", - href: "/invitations", - }, -]; - -function ProjectActionIcons({ type, size, className = "" }: { type: string; size?: number; className?: string }) { - const icons = { - profile: CircleUser, - security: KeyRound, - activity: Activity, - preferences: Settings2, - notifications: Bell, - "api-tokens": KeyRound, - }; - - if (type === undefined) return null; - const Icon = icons[type as keyof typeof icons]; - if (!Icon) return null; - return ; -} - -export const ProfileLayoutSidebar = observer(function ProfileLayoutSidebar() { - // states - const [isSigningOut, setIsSigningOut] = useState(false); - // router - const pathname = usePathname(); - // store hooks - const { sidebarCollapsed, toggleSidebar } = useAppTheme(); - const { data: currentUser, signOut } = useUser(); - const { data: currentUserSettings } = useUserSettings(); - const { workspaces } = useWorkspace(); - const { isMobile } = usePlatformOS(); - const { t } = useTranslation(); - - const workspacesList = Object.values(workspaces ?? {}); - - // redirect url for normal mode - const redirectWorkspaceSlug = - currentUserSettings?.workspace?.last_workspace_slug || - currentUserSettings?.workspace?.fallback_workspace_slug || - ""; - - const ref = useRef(null); - - useOutsideClickDetector(ref, () => { - if (sidebarCollapsed === false) { - if (window.innerWidth < 768) { - toggleSidebar(); - } - } - }); - - useEffect(() => { - const handleResize = () => { - if (window.innerWidth <= 768) { - toggleSidebar(true); - } - }; - handleResize(); - window.addEventListener("resize", handleResize); - return () => { - window.removeEventListener("resize", handleResize); - }; - }, [toggleSidebar]); - - const handleItemClick = () => { - if (window.innerWidth < 768) { - toggleSidebar(); - } - }; - - const handleSignOut = async () => { - setIsSigningOut(true); - await signOut() - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: t("sign_out.toast.error.title"), - message: t("sign_out.toast.error.message"), - }) - ) - .finally(() => setIsSigningOut(false)); - }; - - return ( -
-
- -
- - - - {!sidebarCollapsed && ( -

{t("profile_settings")}

- )} -
- -
- {!sidebarCollapsed && ( -
{t("your_account")}
- )} -
- {PROFILE_ACTION_LINKS.map((link) => { - if (link.key === "change-password" && currentUser?.is_password_autoset) return null; - - return ( - - - -
- - - {!sidebarCollapsed &&

{t(link.i18n_label)}

} -
-
-
- - ); - })} -
-
-
- {!sidebarCollapsed && ( -
{t("workspaces")}
- )} - {workspacesList && workspacesList.length > 0 && ( -
- {workspacesList.map((workspace) => ( - - - - {workspace?.logo_url && workspace.logo_url !== "" ? ( - Workspace Logo - ) : ( - (workspace?.name?.charAt(0) ?? "...") - )} - - {!sidebarCollapsed &&

{workspace.name}

} -
- - ))} -
- )} -
- {WORKSPACE_ACTION_LINKS.map((link) => ( - - -
- {} - {!sidebarCollapsed && t(link.i18n_label)} -
-
- - ))} -
-
-
-
- - - -
-
-
-
- ); -}); diff --git a/apps/web/app/(all)/settings/profile/[profileTabId]/page.tsx b/apps/web/app/(all)/settings/profile/[profileTabId]/page.tsx new file mode 100644 index 0000000000..6b481d8189 --- /dev/null +++ b/apps/web/app/(all)/settings/profile/[profileTabId]/page.tsx @@ -0,0 +1,55 @@ +import { observer } from "mobx-react"; +// plane imports +import { PROFILE_SETTINGS_TABS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import type { TProfileSettingsTabs } from "@plane/types"; +// components +import { LogoSpinner } from "@/components/common/logo-spinner"; +import { PageHead } from "@/components/core/page-title"; +import { ProfileSettingsContent } from "@/components/settings/profile/content"; +import { ProfileSettingsSidebarRoot } from "@/components/settings/profile/sidebar"; +// hooks +import { useUser } from "@/hooks/store/user"; +import { useAppRouter } from "@/hooks/use-app-router"; +// local imports +import type { Route } from "../+types/layout"; + +function ProfileSettingsPage(props: Route.ComponentProps) { + const { profileTabId } = props.params; + // router + const router = useAppRouter(); + // store hooks + const { data: currentUser } = useUser(); + // translation + const { t } = useTranslation(); + // derived values + const isAValidTab = PROFILE_SETTINGS_TABS.includes(profileTabId as TProfileSettingsTabs); + + if (!currentUser || !isAValidTab) + return ( +
+ +
+ ); + + return ( + <> + +
+
+ router.push(`/settings/profile/${tab}`)} + /> + +
+
+ + ); +} + +export default observer(ProfileSettingsPage); diff --git a/apps/web/app/(all)/profile/layout.tsx b/apps/web/app/(all)/settings/profile/layout.tsx similarity index 56% rename from apps/web/app/(all)/profile/layout.tsx rename to apps/web/app/(all)/settings/profile/layout.tsx index f5aebbfbbb..38311cb08d 100644 --- a/apps/web/app/(all)/profile/layout.tsx +++ b/apps/web/app/(all)/settings/profile/layout.tsx @@ -1,20 +1,17 @@ -// components import { Outlet } from "react-router"; -// wrappers +// components import { ProjectsAppPowerKProvider } from "@/components/power-k/projects-app-provider"; +// lib import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper"; -// layout -import { ProfileLayoutSidebar } from "./sidebar"; export default function ProfileSettingsLayout() { return ( <> -
- -
-
+
+
+
diff --git a/apps/web/app/routes/core.ts b/apps/web/app/routes/core.ts index ccb9d78d37..daa83c95f8 100644 --- a/apps/web/app/routes/core.ts +++ b/apps/web/app/routes/core.ts @@ -278,34 +278,6 @@ export const coreRoutes: RouteConfigEntry[] = [ ), ]), - // -------------------------------------------------------------------- - // ACCOUNT SETTINGS - // -------------------------------------------------------------------- - - layout("./(all)/[workspaceSlug]/(settings)/settings/account/layout.tsx", [ - route(":workspaceSlug/settings/account", "./(all)/[workspaceSlug]/(settings)/settings/account/page.tsx"), - route( - ":workspaceSlug/settings/account/activity", - "./(all)/[workspaceSlug]/(settings)/settings/account/activity/page.tsx" - ), - route( - ":workspaceSlug/settings/account/preferences", - "./(all)/[workspaceSlug]/(settings)/settings/account/preferences/page.tsx" - ), - route( - ":workspaceSlug/settings/account/notifications", - "./(all)/[workspaceSlug]/(settings)/settings/account/notifications/page.tsx" - ), - route( - ":workspaceSlug/settings/account/security", - "./(all)/[workspaceSlug]/(settings)/settings/account/security/page.tsx" - ), - route( - ":workspaceSlug/settings/account/api-tokens", - "./(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx" - ), - ]), - // -------------------------------------------------------------------- // PROJECT SETTINGS // -------------------------------------------------------------------- @@ -326,8 +298,24 @@ export const coreRoutes: RouteConfigEntry[] = [ ), // Project Features route( - ":workspaceSlug/settings/projects/:projectId/features", - "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/page.tsx" + ":workspaceSlug/settings/projects/:projectId/features/cycles", + "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/cycles/page.tsx" + ), + route( + ":workspaceSlug/settings/projects/:projectId/features/modules", + "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/modules/page.tsx" + ), + route( + ":workspaceSlug/settings/projects/:projectId/features/views", + "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/views/page.tsx" + ), + route( + ":workspaceSlug/settings/projects/:projectId/features/pages", + "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/pages/page.tsx" + ), + route( + ":workspaceSlug/settings/projects/:projectId/features/intake", + "./(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/features/intake/page.tsx" ), // Project States route( @@ -363,12 +351,8 @@ export const coreRoutes: RouteConfigEntry[] = [ // PROFILE SETTINGS // -------------------------------------------------------------------- - layout("./(all)/profile/layout.tsx", [ - route("profile", "./(all)/profile/page.tsx"), - route("profile/activity", "./(all)/profile/activity/page.tsx"), - route("profile/appearance", "./(all)/profile/appearance/page.tsx"), - route("profile/notifications", "./(all)/profile/notifications/page.tsx"), - route("profile/security", "./(all)/profile/security/page.tsx"), + layout("./(all)/settings/profile/layout.tsx", [ + route("settings/profile/:profileTabId", "./(all)/settings/profile/[profileTabId]/page.tsx"), ]), ]), @@ -389,7 +373,7 @@ export const coreRoutes: RouteConfigEntry[] = [ route(":workspaceSlug/analytics", "routes/redirects/core/analytics.tsx"), // API tokens redirect: /:workspaceSlug/settings/api-tokens - // → /:workspaceSlug/settings/account/api-tokens + // → /settings/profile/api-tokens route(":workspaceSlug/settings/api-tokens", "routes/redirects/core/api-tokens.tsx"), // Inbox redirect: /:workspaceSlug/projects/:projectId/inbox @@ -406,4 +390,10 @@ export const coreRoutes: RouteConfigEntry[] = [ // Register redirect route("register", "routes/redirects/core/register.tsx"), + + // Profile settings redirects + route("profile/*", "routes/redirects/core/profile-settings.tsx"), + + // Account settings redirects + route(":workspaceSlug/settings/account/*", "routes/redirects/core/workspace-account-settings.tsx"), ] satisfies RouteConfig; diff --git a/apps/web/app/routes/redirects/core/api-tokens.tsx b/apps/web/app/routes/redirects/core/api-tokens.tsx index 68007aa416..d97413084b 100644 --- a/apps/web/app/routes/redirects/core/api-tokens.tsx +++ b/apps/web/app/routes/redirects/core/api-tokens.tsx @@ -1,9 +1,7 @@ import { redirect } from "react-router"; -import type { Route } from "./+types/api-tokens"; -export const clientLoader = ({ params }: Route.ClientLoaderArgs) => { - const { workspaceSlug } = params; - throw redirect(`/${workspaceSlug}/settings/account/api-tokens/`); +export const clientLoader = () => { + throw redirect(`/settings/profile/api-tokens/`); }; export default function ApiTokens() { diff --git a/apps/web/app/routes/redirects/core/index.ts b/apps/web/app/routes/redirects/core/index.ts index efd3ae40f8..480386bf62 100644 --- a/apps/web/app/routes/redirects/core/index.ts +++ b/apps/web/app/routes/redirects/core/index.ts @@ -14,7 +14,7 @@ export const coreRedirectRoutes: RouteConfigEntry[] = [ route(":workspaceSlug/analytics", "routes/redirects/core/analytics.tsx"), // API tokens redirect: /:workspaceSlug/settings/api-tokens - // → /:workspaceSlug/settings/account/api-tokens + // → /settings/profile/api-tokens route(":workspaceSlug/settings/api-tokens", "routes/redirects/core/api-tokens.tsx"), // Inbox redirect: /:workspaceSlug/projects/:projectId/inbox diff --git a/apps/web/app/routes/redirects/core/profile-settings.tsx b/apps/web/app/routes/redirects/core/profile-settings.tsx new file mode 100644 index 0000000000..7e8a0c15a2 --- /dev/null +++ b/apps/web/app/routes/redirects/core/profile-settings.tsx @@ -0,0 +1,12 @@ +import { redirect } from "react-router"; +import type { Route } from "./+types/profile-settings"; + +export const clientLoader = ({ params, request }: Route.ClientLoaderArgs) => { + const searchParams = new URL(request.url).searchParams; + const splat = params["*"] || ""; + throw redirect(`/settings/profile/${splat || "general"}?${searchParams.toString()}`); +}; + +export default function ProfileSettings() { + return null; +} diff --git a/apps/web/app/routes/redirects/core/workspace-account-settings.tsx b/apps/web/app/routes/redirects/core/workspace-account-settings.tsx new file mode 100644 index 0000000000..10d375e9a1 --- /dev/null +++ b/apps/web/app/routes/redirects/core/workspace-account-settings.tsx @@ -0,0 +1,12 @@ +import { redirect } from "react-router"; +import type { Route } from "./+types/workspace-account-settings"; + +export const clientLoader = ({ params, request }: Route.ClientLoaderArgs) => { + const searchParams = new URL(request.url).searchParams; + const splat = params["*"] || ""; + throw redirect(`/settings/profile/${splat || "general"}?${searchParams.toString()}`); +}; + +export default function WorkspaceAccountSettings() { + return null; +} diff --git a/apps/web/ce/components/common/modal/global.tsx b/apps/web/ce/components/common/modal/global.tsx new file mode 100644 index 0000000000..76b859c3e8 --- /dev/null +++ b/apps/web/ce/components/common/modal/global.tsx @@ -0,0 +1,26 @@ +import { lazy, Suspense } from "react"; +import { observer } from "mobx-react"; + +const ProfileSettingsModal = lazy(() => + import("@/components/settings/profile/modal").then((module) => ({ + default: module.ProfileSettingsModal, + })) +); + +type TGlobalModalsProps = { + workspaceSlug: string; +}; + +/** + * GlobalModals component manages all workspace-level modals across Plane applications. + * + * This includes: + * - Profile settings modal + */ +export const GlobalModals = observer(function GlobalModals(_props: TGlobalModalsProps) { + return ( + + + + ); +}); diff --git a/apps/web/ce/components/navigations/top-navigation-root.tsx b/apps/web/ce/components/navigations/top-navigation-root.tsx index 9035e3147e..1ee0d9e192 100644 --- a/apps/web/ce/components/navigations/top-navigation-root.tsx +++ b/apps/web/ce/components/navigations/top-navigation-root.tsx @@ -74,7 +74,7 @@ export const TopNavigationRoot = observer(function TopNavigationRoot() {
- +
diff --git a/apps/web/ce/components/preferences/config.ts b/apps/web/ce/components/preferences/config.ts deleted file mode 100644 index 1a67ab7d34..0000000000 --- a/apps/web/ce/components/preferences/config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { StartOfWeekPreference } from "@/components/profile/start-of-week-preference"; -import { ThemeSwitcher } from "./theme-switcher"; - -export const PREFERENCE_COMPONENTS = { - theme: ThemeSwitcher, - start_of_week: StartOfWeekPreference, -}; diff --git a/apps/web/ce/components/preferences/theme-switcher.tsx b/apps/web/ce/components/preferences/theme-switcher.tsx index d6e6dc252c..b2c2008b1a 100644 --- a/apps/web/ce/components/preferences/theme-switcher.tsx +++ b/apps/web/ce/components/preferences/theme-switcher.tsx @@ -10,8 +10,7 @@ import { applyCustomTheme } from "@plane/utils"; // components import { CustomThemeSelector } from "@/components/core/theme/custom-theme-selector"; import { ThemeSwitch } from "@/components/core/theme/theme-switch"; -// helpers -import { PreferencesSection } from "@/components/preferences/section"; +import { SettingsControlItem } from "@/components/settings/control-item"; // hooks import { useUserProfile } from "@/hooks/store/user"; @@ -79,18 +78,16 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: { return ( <> - - { - void handleThemeChange(themeOption); - }} - /> -
+ { + void handleThemeChange(themeOption); + }} + /> } /> {userProfile.theme?.theme === "custom" && } diff --git a/apps/web/ce/components/workspace/billing/root.tsx b/apps/web/ce/components/workspace/billing/root.tsx index 2753887d17..ff6b243b9e 100644 --- a/apps/web/ce/components/workspace/billing/root.tsx +++ b/apps/web/ce/components/workspace/billing/root.tsx @@ -6,6 +6,7 @@ import { useTranslation } from "@plane/i18n"; import type { TBillingFrequency, TProductBillingFrequency } from "@plane/types"; import { EProductSubscriptionEnum } from "@plane/types"; // components +import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item"; import { SettingsHeading } from "@/components/settings/heading"; // local imports import { PlansComparison } from "./comparison/root"; @@ -37,32 +38,28 @@ export const BillingRoot = observer(function BillingRoot() { setProductBillingFrequency({ ...productBillingFrequency, [subscriptionType]: frequency }); return ( -
- +
-
-
-
-
-

Community

-
- Unlimited projects, issues, cycles, modules, pages, and storage -
-
-
-
+ +
+
-
All plans
- +
+

All plans

+ +
); }); diff --git a/apps/web/ce/components/workspace/delete-workspace-section.tsx b/apps/web/ce/components/workspace/delete-workspace-section.tsx index 8fb5999291..cc4ab51ecc 100644 --- a/apps/web/ce/components/workspace/delete-workspace-section.tsx +++ b/apps/web/ce/components/workspace/delete-workspace-section.tsx @@ -1,15 +1,14 @@ import { useState } from "react"; import { observer } from "mobx-react"; -// types +// plane imports import { WORKSPACE_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; -import { ChevronDownIcon, ChevronUpIcon } from "@plane/propel/icons"; import type { IWorkspace } from "@plane/types"; -// ui -import { Collapsible } from "@plane/ui"; -import { DeleteWorkspaceModal } from "./delete-workspace-modal"; // components +import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item"; +// local imports +import { DeleteWorkspaceModal } from "./delete-workspace-modal"; type TDeleteWorkspace = { workspace: IWorkspace | null; @@ -18,8 +17,8 @@ type TDeleteWorkspace = { export const DeleteWorkspaceSection = observer(function DeleteWorkspaceSection(props: TDeleteWorkspace) { const { workspace } = props; // states - const [isOpen, setIsOpen] = useState(false); const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState(false); + // translation const { t } = useTranslation(); return ( @@ -29,40 +28,19 @@ export const DeleteWorkspaceSection = observer(function DeleteWorkspaceSection(p isOpen={deleteWorkspaceModal} onClose={() => setDeleteWorkspaceModal(false)} /> -
-
- setIsOpen(!isOpen)} - className="w-full" - buttonClassName="flex w-full items-center justify-between py-4" - title={ - <> - - {t("workspace_settings.settings.general.delete_workspace")} - - {isOpen ? : } - - } + setDeleteWorkspaceModal(true)} + data-ph-element={WORKSPACE_TRACKER_ELEMENTS.DELETE_WORKSPACE_BUTTON} > -
- - {t("workspace_settings.settings.general.delete_workspace_description")} - -
- -
-
-
-
-
+ {t("delete")} + + } + /> ); }); diff --git a/apps/web/ce/constants/project/settings/index.ts b/apps/web/ce/constants/project/settings/index.ts index a6a842e7be..0e849261ac 100644 --- a/apps/web/ce/constants/project/settings/index.ts +++ b/apps/web/ce/constants/project/settings/index.ts @@ -1,2 +1 @@ export * from "./features"; -export * from "./tabs"; diff --git a/apps/web/ce/constants/project/settings/tabs.ts b/apps/web/ce/constants/project/settings/tabs.ts deleted file mode 100644 index f78b51a74f..0000000000 --- a/apps/web/ce/constants/project/settings/tabs.ts +++ /dev/null @@ -1,82 +0,0 @@ -// icons -import { EUserPermissions } from "@plane/constants"; -import { SettingIcon } from "@/components/icons/attachment"; -// types -import type { Props } from "@/components/icons/types"; -// constants - -export const PROJECT_SETTINGS = { - general: { - key: "general", - i18n_label: "common.general", - href: ``, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/`, - Icon: SettingIcon, - }, - members: { - key: "members", - i18n_label: "common.members", - href: `/members`, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/members/`, - Icon: SettingIcon, - }, - features: { - key: "features", - i18n_label: "common.features", - href: `/features`, - access: [EUserPermissions.ADMIN], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/features/`, - Icon: SettingIcon, - }, - states: { - key: "states", - i18n_label: "common.states", - href: `/states`, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/states/`, - Icon: SettingIcon, - }, - labels: { - key: "labels", - i18n_label: "common.labels", - href: `/labels`, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/labels/`, - Icon: SettingIcon, - }, - estimates: { - key: "estimates", - i18n_label: "common.estimates", - href: `/estimates`, - access: [EUserPermissions.ADMIN], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/estimates/`, - Icon: SettingIcon, - }, - automations: { - key: "automations", - i18n_label: "project_settings.automations.label", - href: `/automations`, - access: [EUserPermissions.ADMIN], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/automations/`, - Icon: SettingIcon, - }, -}; - -export const PROJECT_SETTINGS_LINKS: { - key: string; - i18n_label: string; - href: string; - access: EUserPermissions[]; - highlight: (pathname: string, baseUrl: string) => boolean; - Icon: React.FC; -}[] = [ - PROJECT_SETTINGS["general"], - PROJECT_SETTINGS["members"], - PROJECT_SETTINGS["features"], - PROJECT_SETTINGS["states"], - PROJECT_SETTINGS["labels"], - PROJECT_SETTINGS["estimates"], - PROJECT_SETTINGS["automations"], -]; diff --git a/apps/web/core/components/appearance/index.ts b/apps/web/core/components/appearance/index.ts new file mode 100644 index 0000000000..1d07ba69ea --- /dev/null +++ b/apps/web/core/components/appearance/index.ts @@ -0,0 +1 @@ +export * from "./theme-switcher"; diff --git a/apps/web/core/components/appearance/theme-switcher.tsx b/apps/web/core/components/appearance/theme-switcher.tsx new file mode 100644 index 0000000000..993a06df58 --- /dev/null +++ b/apps/web/core/components/appearance/theme-switcher.tsx @@ -0,0 +1,70 @@ +import { useCallback, useMemo } from "react"; +import { observer } from "mobx-react"; +import { useTheme } from "next-themes"; +// plane imports +import type { I_THEME_OPTION } from "@plane/constants"; +import { THEME_OPTIONS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { setPromiseToast } from "@plane/propel/toast"; +// components +import { CustomThemeSelector } from "@/components/core/theme/custom-theme-selector"; +import { ThemeSwitch } from "@/components/core/theme/theme-switch"; +import { SettingsControlItem } from "@/components/settings/control-item"; +// hooks +import { useUserProfile } from "@/hooks/store/user"; + +export const ThemeSwitcher = observer(function ThemeSwitcher(props: { + option: { + id: string; + title: string; + description: string; + }; +}) { + // store hooks + const { data: userProfile, updateUserTheme } = useUserProfile(); + // theme + const { setTheme } = useTheme(); + // translation + const { t } = useTranslation(); + // derived values + const currentTheme = useMemo(() => { + const userThemeOption = THEME_OPTIONS.find((t) => t.value === userProfile?.theme?.theme); + return userThemeOption || null; + }, [userProfile?.theme?.theme]); + + const handleThemeChange = useCallback( + (themeOption: I_THEME_OPTION) => { + try { + setTheme(themeOption.value); + const updatePromise = updateUserTheme({ theme: themeOption.value }); + setPromiseToast(updatePromise, { + loading: "Updating theme...", + success: { + title: "Success!", + message: () => "Theme updated successfully!", + }, + error: { + title: "Error!", + message: () => "Failed to update the theme", + }, + }); + } catch (error) { + console.error("Error updating theme:", error); + } + }, + [updateUserTheme] + ); + + if (!userProfile) return null; + + return ( + <> + } + /> + {userProfile.theme?.theme === "custom" && } + + ); +}); diff --git a/apps/web/core/components/automation/auto-archive-automation.tsx b/apps/web/core/components/automation/auto-archive-automation.tsx index 9d3e14afd5..dd01cca409 100644 --- a/apps/web/core/components/automation/auto-archive-automation.tsx +++ b/apps/web/core/components/automation/auto-archive-automation.tsx @@ -2,14 +2,14 @@ import { useMemo, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { ArchiveRestore } from "lucide-react"; -// types +// plane imports import { PROJECT_AUTOMATION_MONTHS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import type { IProject } from "@plane/types"; -// ui import { CustomSelect, Loader, ToggleSwitch } from "@plane/ui"; // component import { SelectMonthModal } from "@/components/automation"; +import { SettingsControlItem } from "@/components/settings/control-item"; // hooks import { useProject } from "@/hooks/store/use-project"; import { useUserPermissions } from "@/hooks/store/user"; @@ -61,25 +61,22 @@ export const AutoArchiveAutomation = observer(function AutoArchiveAutomation(pro handleClose={() => setmonthModal(false)} handleChange={handleChange} /> -
-
-
-
- -
-
-

{t("project_settings.automations.auto-archive.title")}

-

- {t("project_settings.automations.auto-archive.description")} -

-
+
+
+
+
- + + } + />
- {currentProjectDetails ? ( autoArchiveStatus && ( -
+
{t("project_settings.automations.auto-archive.duration")} @@ -90,9 +87,7 @@ export const AutoArchiveAutomation = observer(function AutoArchiveAutomation(pro label={`${currentProjectDetails?.archive_in} ${ currentProjectDetails?.archive_in === 1 ? "month" : "months" }`} - onChange={(val: number) => { - handleChange({ archive_in: val }); - }} + onChange={(val: number) => void handleChange({ archive_in: val })} input disabled={!isAdmin} > @@ -117,7 +112,7 @@ export const AutoArchiveAutomation = observer(function AutoArchiveAutomation(pro
) ) : ( - + )} diff --git a/apps/web/core/components/automation/auto-close-automation.tsx b/apps/web/core/components/automation/auto-close-automation.tsx index 6b9e7a8d42..7ceaba3b02 100644 --- a/apps/web/core/components/automation/auto-close-automation.tsx +++ b/apps/web/core/components/automation/auto-close-automation.tsx @@ -1,18 +1,15 @@ import { useMemo, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// icons import { ArchiveX } from "lucide-react"; -// types +// plane imports import { PROJECT_AUTOMATION_MONTHS, EUserPermissions, EUserPermissionsLevel, EIconSize } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { StateGroupIcon, StatePropertyIcon } from "@plane/propel/icons"; import type { IProject } from "@plane/types"; -// ui import { CustomSelect, CustomSearchSelect, ToggleSwitch, Loader } from "@plane/ui"; -// component import { SelectMonthModal } from "@/components/automation"; -// constants +import { SettingsControlItem } from "@/components/settings/control-item"; // hooks import { useProject } from "@/hooks/store/use-project"; import { useProjectState } from "@/hooks/store/use-project-state"; @@ -82,36 +79,34 @@ export const AutoCloseAutomation = observer(function AutoCloseAutomation(props: handleClose={() => setmonthModal(false)} handleChange={handleChange} /> -
-
-
-
- -
-
-

{t("project_settings.automations.auto-close.title")}

-

- {t("project_settings.automations.auto-close.description")} -

-
+
+
+
+
- { - if (currentProjectDetails?.close_in === 0) { - await handleChange({ close_in: 1, default_state: defaultState }); - } else { - await handleChange({ close_in: 0, default_state: null }); - } - }} - size="sm" - disabled={!isAdmin} + { + if (currentProjectDetails?.close_in === 0) { + void handleChange({ close_in: 1, default_state: defaultState }); + } else { + void handleChange({ close_in: 0, default_state: null }); + } + }} + size="sm" + disabled={!isAdmin} + /> + } />
{currentProjectDetails ? ( autoCloseStatus && ( -
+
@@ -123,9 +118,7 @@ export const AutoCloseAutomation = observer(function AutoCloseAutomation(props: label={`${currentProjectDetails?.close_in} ${ currentProjectDetails?.close_in === 1 ? "month" : "months" }`} - onChange={(val: number) => { - handleChange({ close_in: val }); - }} + onChange={(val: number) => void handleChange({ close_in: val })} input disabled={!isAdmin} > @@ -176,9 +169,7 @@ export const AutoCloseAutomation = observer(function AutoCloseAutomation(props: : (currentDefaultState?.name ?? {t("state")})}
} - onChange={(val: string) => { - handleChange({ default_state: val }); - }} + onChange={(val: string) => void handleChange({ default_state: val })} options={options} disabled={!multipleOptions} input @@ -189,7 +180,7 @@ export const AutoCloseAutomation = observer(function AutoCloseAutomation(props:
) ) : ( - + )} diff --git a/apps/web/core/components/core/theme/color-inputs.tsx b/apps/web/core/components/core/theme/color-inputs.tsx new file mode 100644 index 0000000000..3459a203a0 --- /dev/null +++ b/apps/web/core/components/core/theme/color-inputs.tsx @@ -0,0 +1,92 @@ +import { observer } from "mobx-react"; +import type { Control } from "react-hook-form"; +import { Controller } from "react-hook-form"; +// plane imports +import type { IUserTheme } from "@plane/types"; +import { InputColorPicker } from "@plane/ui"; + +type Props = { + control: Control; +}; + +export const CustomThemeColorInputs = observer(function CustomThemeColorInputs(props: Props) { + const { control } = props; + + const handleValueChange = (val: string | undefined, onChange: (...args: unknown[]) => void) => { + let hex = val; + // prepend a hashtag if it doesn't exist + if (val && val[0] !== "#") hex = `#${val}`; + onChange(hex); + }; + + return ( +
+ {/* Neutral Color */} +
+

+ Neutral color* +

+
+ ( + handleValueChange(val, onChange)} + placeholder="#1a1a1a" + className="w-full placeholder:text-placeholder" + style={{ + backgroundColor: value, + color: "#ffffff", + }} + hasError={false} + /> + )} + /> +
+
+ {/* Brand Color */} +
+

+ Brand color* +

+
+ ( + handleValueChange(val, onChange)} + placeholder="#3f76ff" + className="w-full placeholder:text-placeholder" + style={{ + backgroundColor: value, + color: "#ffffff", + }} + hasError={false} + /> + )} + /> +
+
+
+ ); +}); diff --git a/apps/web/core/components/core/theme/custom-theme-selector.tsx b/apps/web/core/components/core/theme/custom-theme-selector.tsx index 1091966e1c..e498840b69 100644 --- a/apps/web/core/components/core/theme/custom-theme-selector.tsx +++ b/apps/web/core/components/core/theme/custom-theme-selector.tsx @@ -1,17 +1,21 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react"; -import { Controller, useForm } from "react-hook-form"; +import { useForm } from "react-hook-form"; // plane imports import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { IUserTheme } from "@plane/types"; -import { InputColorPicker, ToggleSwitch } from "@plane/ui"; import { applyCustomTheme } from "@plane/utils"; +// components +import { ProfileSettingsHeading } from "@/components/settings/profile/heading"; // hooks import { useUserProfile } from "@/hooks/store/user"; // local imports -import { CustomThemeConfigHandler } from "./config-handler"; +import { CustomThemeColorInputs } from "./color-inputs"; +import { CustomThemeDownloadConfigButton } from "./download-config-button"; +import { CustomThemeImportConfigButton } from "./import-config-button"; +import { CustomThemeModeSelector } from "./theme-mode-selector"; export const CustomThemeSelector = observer(function CustomThemeSelector() { // store hooks @@ -23,18 +27,17 @@ export const CustomThemeSelector = observer(function CustomThemeSelector() { const [isLoadingPalette, setIsLoadingPalette] = useState(false); // Load saved theme from userProfile (fallback to defaults) - const getSavedTheme = (): IUserTheme => { - if (userProfile?.theme) { - const theme = userProfile.theme; - if (theme.primary && theme.background && theme.darkPalette !== undefined) { - return { - theme: "custom", - primary: theme.primary, - background: theme.background, - darkPalette: theme.darkPalette, - }; - } + const savedTheme = useMemo((): IUserTheme => { + const theme = userProfile?.theme; + if (theme && theme.primary && theme.background) { + return { + theme: "custom", + primary: theme.primary, + background: theme.background, + darkPalette: !!theme.darkPalette, + }; } + // Fallback to defaults return { theme: "custom", @@ -42,21 +45,20 @@ export const CustomThemeSelector = observer(function CustomThemeSelector() { background: "#1a1a1a", darkPalette: false, }; - }; + }, [userProfile?.theme]); const { control, formState: { isSubmitting }, handleSubmit, getValues, - watch, setValue, } = useForm({ - defaultValues: getSavedTheme(), + defaultValues: savedTheme, }); const handleUpdateTheme = async (formData: IUserTheme) => { - if (!formData.primary || !formData.background || formData.darkPalette === undefined) return; + if (!formData.primary || !formData.background) return; try { setIsLoadingPalette(true); @@ -90,109 +92,29 @@ export const CustomThemeSelector = observer(function CustomThemeSelector() { } }; - const handleValueChange = (val: string | undefined, onChange: (...args: unknown[]) => void) => { - let hex = val; - // prepend a hashtag if it doesn't exist - if (val && val[0] !== "#") hex = `#${val}`; - onChange(hex); - }; - return ( -
+ { + void handleSubmit(handleUpdateTheme)(e); + }} + className="bg-layer-1 border border-subtle rounded-lg py-3 px-4" + >
-

{t("customize_your_theme")}

- -
- {/* Color Inputs */} -
- {/* Brand Color */} -
-

Brand color

-
- ( - handleValueChange(val, onChange)} - placeholder="#3f76ff" - className="w-full placeholder:text-placeholder" - style={{ - backgroundColor: value, - color: "#ffffff", - }} - hasError={false} - /> - )} - /> -
-
- - {/* Neutral Color */} -
-

Neutral color

-
- ( - handleValueChange(val, onChange)} - placeholder="#1a1a1a" - className="w-full placeholder:text-placeholder" - style={{ - backgroundColor: value, - color: "#ffffff", - }} - hasError={false} - /> - )} - /> -
-
-
-
+ } + /> + + {/* Color Inputs */} +
-
+ {/* Save Theme Button */} + {/* Import/Export Section */} - - -
- {/* Theme Mode Toggle */} -
- ( - - )} - /> - {watch("darkPalette") ? "Dark mode" : "Light mode"} -
- {/* Save Theme Button */} - -
+
); diff --git a/apps/web/core/components/core/theme/download-config-button.tsx b/apps/web/core/components/core/theme/download-config-button.tsx new file mode 100644 index 0000000000..0a80a2aa2a --- /dev/null +++ b/apps/web/core/components/core/theme/download-config-button.tsx @@ -0,0 +1,59 @@ +import { observer } from "mobx-react"; +import type { UseFormGetValues } from "react-hook-form"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { Button } from "@plane/propel/button"; +import { setToast, TOAST_TYPE } from "@plane/propel/toast"; +import type { IUserTheme } from "@plane/types"; + +type Props = { + getValues: UseFormGetValues; +}; + +export const CustomThemeDownloadConfigButton = observer(function CustomThemeDownloadConfigButton(props: Props) { + const { getValues } = props; + // translation + const { t } = useTranslation(); + + const handleDownloadConfig = () => { + try { + const currentValues = getValues(); + const config = { + version: "1.0", + themeName: "Custom Theme", + primary: currentValues.primary, + background: currentValues.background, + darkPalette: currentValues.darkPalette, + }; + + const blob = new Blob([JSON.stringify(config, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `plane-theme-${Date.now()}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("success"), + message: "Theme configuration downloaded successfully.", + }); + } catch (error) { + console.error("Failed to download config:", error); + setToast({ + type: TOAST_TYPE.ERROR, + title: t("error"), + message: "Failed to download theme configuration.", + }); + } + }; + + return ( + + ); +}); diff --git a/apps/web/core/components/core/theme/config-handler.tsx b/apps/web/core/components/core/theme/import-config-button.tsx similarity index 62% rename from apps/web/core/components/core/theme/config-handler.tsx rename to apps/web/core/components/core/theme/import-config-button.tsx index a426c75079..c585e3c507 100644 --- a/apps/web/core/components/core/theme/config-handler.tsx +++ b/apps/web/core/components/core/theme/import-config-button.tsx @@ -1,6 +1,6 @@ import { useRef } from "react"; import { observer } from "mobx-react"; -import type { UseFormGetValues, UseFormSetValue } from "react-hook-form"; +import type { UseFormSetValue } from "react-hook-form"; // plane imports import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; @@ -8,54 +8,17 @@ import { setToast, TOAST_TYPE } from "@plane/propel/toast"; import type { IUserTheme } from "@plane/types"; type Props = { - getValues: UseFormGetValues; handleUpdateTheme: (formData: IUserTheme) => Promise; setValue: UseFormSetValue; }; -export const CustomThemeConfigHandler = observer(function CustomThemeConfigHandler(props: Props) { - const { getValues, handleUpdateTheme, setValue } = props; +export const CustomThemeImportConfigButton = observer(function CustomThemeImportConfigButton(props: Props) { + const { handleUpdateTheme, setValue } = props; // refs const fileInputRef = useRef(null); // translation const { t } = useTranslation(); - const handleDownloadConfig = () => { - try { - const currentValues = getValues(); - const config = { - version: "1.0", - themeName: "Custom Theme", - primary: currentValues.primary, - background: currentValues.background, - darkPalette: currentValues.darkPalette, - }; - - const blob = new Blob([JSON.stringify(config, null, 2)], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `plane-theme-${Date.now()}.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - - setToast({ - type: TOAST_TYPE.SUCCESS, - title: t("success"), - message: "Theme configuration downloaded successfully.", - }); - } catch (error) { - console.error("Failed to download config:", error); - setToast({ - type: TOAST_TYPE.ERROR, - title: t("error"), - message: "Failed to download theme configuration.", - }); - } - }; - const handleUploadConfig = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; @@ -122,14 +85,11 @@ export const CustomThemeConfigHandler = observer(function CustomThemeConfigHandl }; return ( -
+ <> - - -
+ ); }); diff --git a/apps/web/core/components/core/theme/theme-mode-selector.tsx b/apps/web/core/components/core/theme/theme-mode-selector.tsx new file mode 100644 index 0000000000..d791a14864 --- /dev/null +++ b/apps/web/core/components/core/theme/theme-mode-selector.tsx @@ -0,0 +1,51 @@ +import { observer } from "mobx-react"; +import type { Control } from "react-hook-form"; +import { Controller } from "react-hook-form"; +// plane imports +import type { IUserTheme } from "@plane/types"; + +type Props = { + control: Control; +}; + +export const CustomThemeModeSelector = observer(function CustomThemeModeSelector(props: Props) { + const { control } = props; + + return ( +
+
+ Choose color mode* +
+ ( +
+ + +
+ )} + /> +
+ ); +}); diff --git a/apps/web/core/components/core/theme/theme-switch.tsx b/apps/web/core/components/core/theme/theme-switch.tsx index a6cd5cb41f..d0a0ac3b6d 100644 --- a/apps/web/core/components/core/theme/theme-switch.tsx +++ b/apps/web/core/components/core/theme/theme-switch.tsx @@ -50,6 +50,7 @@ export function ThemeSwitch(props: Props) { ) } onChange={onChange} + buttonClassName="border border-subtle-1" placement="bottom-end" input > diff --git a/apps/web/core/components/estimates/estimate-list-item.tsx b/apps/web/core/components/estimates/estimate-list-item.tsx index b8f951192b..0ae6888866 100644 --- a/apps/web/core/components/estimates/estimate-list-item.tsx +++ b/apps/web/core/components/estimates/estimate-list-item.tsx @@ -1,12 +1,13 @@ -import type { FC } from "react"; import { observer } from "mobx-react"; +// plane imports import { EEstimateSystem } from "@plane/constants"; -import { convertMinutesToHoursMinutesString, cn } from "@plane/utils"; -// helpers +import { convertMinutesToHoursMinutesString } from "@plane/utils"; +// components +import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item"; // hooks import { useProjectEstimates } from "@/hooks/store/estimates"; import { useEstimate } from "@/hooks/store/estimates/use-estimate"; -// plane web components +// plane web imports import { EstimateListItemButtons } from "@/plane-web/components/estimates"; type TEstimateListItem = { @@ -19,40 +20,31 @@ type TEstimateListItem = { }; export const EstimateListItem = observer(function EstimateListItem(props: TEstimateListItem) { - const { estimateId, isAdmin, isEstimateEnabled, isEditable } = props; - // hooks + const { estimateId } = props; + // store hooks const { estimateById } = useProjectEstimates(); const { estimatePointIds, estimatePointById } = useEstimate(estimateId); const currentEstimate = estimateById(estimateId); - // derived values const estimatePointValues = estimatePointIds?.map((estimatePointId) => { const estimatePoint = estimatePointById(estimatePointId); if (estimatePoint) return estimatePoint.value; }); - if (!currentEstimate) return <>; + if (!currentEstimate) return null; + return ( -
-
-

{currentEstimate?.name}

-

- {estimatePointValues - ?.map((estimatePointValue) => { - if (currentEstimate?.type === EEstimateSystem.TIME) { - return convertMinutesToHoursMinutesString(Number(estimatePointValue)); - } - return estimatePointValue; - }) - .join(", ")} -

-
- -
+ { + if (currentEstimate.type === EEstimateSystem.TIME) { + return convertMinutesToHoursMinutesString(Number(estimatePointValue)); + } + return estimatePointValue; + }) + .join(", ")} + control={} + /> ); }); diff --git a/apps/web/core/components/estimates/root.tsx b/apps/web/core/components/estimates/root.tsx index 3a2310b581..53b7e05e41 100644 --- a/apps/web/core/components/estimates/root.tsx +++ b/apps/web/core/components/estimates/root.tsx @@ -1,9 +1,11 @@ -import type { FC } from "react"; import { useState } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; // plane imports import { useTranslation } from "@plane/i18n"; +// components +import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item"; +import { SettingsHeading } from "@/components/settings/heading"; // hooks import { EmptyStateCompact } from "@plane/propel/empty-state"; import { useProjectEstimates } from "@/hooks/store/estimates"; @@ -11,7 +13,6 @@ import { useProject } from "@/hooks/store/use-project"; // plane web components import { UpdateEstimateModal } from "@/plane-web/components/estimates"; // local imports -import { SettingsHeading } from "../settings/heading"; import { CreateEstimateModal } from "./create/modal"; import { DeleteEstimateModal } from "./delete/modal"; import { EstimateDisableSwitch } from "./estimate-disable-switch"; @@ -41,40 +42,43 @@ export const EstimateRoot = observer(function EstimateRoot(props: TEstimateRoot) async () => workspaceSlug && projectId && getProjectEstimates(workspaceSlug, projectId) ); + if (loader === "init-loader" || isSWRLoading) { + return ; + } + return ( -
- {loader === "init-loader" || isSWRLoading ? ( - - ) : ( -
- {/* header */} - - - + <> +
+ {/* header */} + +
{/* current active estimate section */} {currentActiveEstimateId ? ( -
+ <> {/* estimates activated deactivated section */} -
-
-

{t("project_settings.estimates.title")}

-

{t("project_settings.estimates.enable_description")}

-
- -
- {/* active estimates section */} - setEstimateToUpdate(estimateId)} - onDeleteClick={(estimateId: string) => setEstimateToDelete(estimateId)} + + } /> -
+ {/* active estimates section */} +
+ + setEstimateToUpdate(estimateId)} + onDeleteClick={(estimateId: string) => setEstimateToDelete(estimateId)} + /> +
+ ) : ( )} - {/* archived estimates section */} {archivedEstimateIds && archivedEstimateIds.length > 0 && ( -
-
-

Archived estimates

-

- Estimates have gone through a change, these are the estimates you had in your older versions which - were not in use. Read more about them  - - here. - -

-
+
+ + Estimates have gone through a change, these are the estimates you had in your older versions which + were not in use. Read more about them  + + here. + + + } + variant="h6" + />
)}
- )} - +
{/* CRUD modals */} setEstimateToDelete(undefined)} /> -
+ ); }); diff --git a/apps/web/core/components/exporter/export-form.tsx b/apps/web/core/components/exporter/export-form.tsx index c56e462b50..d71014bb24 100644 --- a/apps/web/core/components/exporter/export-form.tsx +++ b/apps/web/core/components/exporter/export-form.tsx @@ -21,6 +21,8 @@ import { CustomSearchSelect, CustomSelect } from "@plane/ui"; import { useProject } from "@/hooks/store/use-project"; import { useUser, useUserPermissions } from "@/hooks/store/user"; import { ProjectExportService } from "@/services/project/project-export.service"; +// local imports +import { SettingsBoxedControlItem } from "../settings/boxed-control-item"; type Props = { workspaceSlug: string; @@ -134,68 +136,75 @@ export const ExportForm = observer(function ExportForm(props: Props) { onSubmit={(e) => { void handleSubmit(ExportCSVToMail)(e); }} - className="flex flex-col gap-4 mt-4" + className="flex flex-col gap-5" > -
+
{/* Project Selector */} -
-
- {t("workspace_settings.settings.exports.exporting_projects")} -
- ( - onChange(val)} - options={options} - input - label={ - value && value.length > 0 - ? value - .map((projectId) => { - const projectDetails = getProjectById(projectId); + ( + onChange(val)} + options={options} + input + label={ + value && value.length > 0 + ? value + .map((projectId) => { + const projectDetails = getProjectById(projectId); - return projectDetails?.identifier; - }) - .join(", ") - : "All projects" - } - optionsClassName="max-w-48 sm:max-w-[532px]" - placement="bottom-end" - multiple - /> - )} - /> -
+ return projectDetails?.identifier; + }) + .join(", ") + : "All projects" + } + optionsClassName="max-w-48 sm:max-w-[532px]" + placement="bottom-end" + multiple + /> + )} + /> + } + /> {/* Format Selector */} -
-
- {t("workspace_settings.settings.exports.format")} -
- ( - - {EXPORTERS_LIST.map((service) => ( - - {t(service.i18n_title)} - - ))} - - )} - /> + ( + + {EXPORTERS_LIST.map((service) => ( + + {t(service.i18n_title)} + + ))} + + )} + /> + } + /> +
+
{/* Rich Filters */} @@ -241,11 +250,6 @@ export const ExportForm = observer(function ExportForm(props: Props) { )} />
*/} -
- -
); }); diff --git a/apps/web/core/components/exporter/guide.tsx b/apps/web/core/components/exporter/guide.tsx index b35a9125f0..7861f0d9b0 100644 --- a/apps/web/core/components/exporter/guide.tsx +++ b/apps/web/core/components/exporter/guide.tsx @@ -2,11 +2,13 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { useParams, useSearchParams } from "next/navigation"; import { mutate } from "swr"; +// constants import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys"; +// local imports import { ExportForm } from "./export-form"; import { PrevExports } from "./prev-exports"; -const IntegrationGuide = observer(function IntegrationGuide() { +export const ExportGuide = observer(function ExportGuide() { // router const { workspaceSlug } = useParams(); const searchParams = useSearchParams(); @@ -17,18 +19,14 @@ const IntegrationGuide = observer(function IntegrationGuide() { return ( <> -
- <> - mutate(EXPORT_SERVICES_LIST(workspaceSlug, `${cursor}`, `${per_page}`))} - /> - - +
+ mutate(EXPORT_SERVICES_LIST(workspaceSlug, `${cursor}`, `${per_page}`))} + /> +
); }); - -export default IntegrationGuide; diff --git a/apps/web/core/components/exporter/prev-exports.tsx b/apps/web/core/components/exporter/prev-exports.tsx index dbb8dbb73a..d777ef8a45 100644 --- a/apps/web/core/components/exporter/prev-exports.tsx +++ b/apps/web/core/components/exporter/prev-exports.tsx @@ -59,11 +59,9 @@ export const PrevExports = observer(function PrevExports(props: Props) { return (
-
+
-

- {t("workspace_settings.settings.exports.previous_exports")} -

+

{t("workspace_settings.settings.exports.previous_exports")}

)}
-
{exporterServices && exporterServices?.results ? ( exporterServices?.results?.length > 0 ? ( diff --git a/apps/web/core/components/global/timezone-select.tsx b/apps/web/core/components/global/timezone-select.tsx index 59f3285e75..1a25a022a6 100644 --- a/apps/web/core/components/global/timezone-select.tsx +++ b/apps/web/core/components/global/timezone-select.tsx @@ -1,5 +1,5 @@ -import type { FC } from "react"; import { observer } from "mobx-react"; +// plane imports import { CustomSearchSelect } from "@plane/ui"; import { cn } from "@plane/utils"; // hooks @@ -38,13 +38,14 @@ export const TimezoneSelect = observer(function TimezoneSelect(props: TTimezoneS label={value && selectedValue ? selectedValue(value) : label} options={isDisabled || disabled ? [] : timezones} onChange={onChange} - buttonClassName={cn(buttonClassName, { + buttonClassName={cn(buttonClassName, "border border-subtle-1", { "border-danger-strong": error, })} - className={cn("rounded-md border-[0.5px] !border-subtle", className)} + className={cn("rounded-md", className)} optionsClassName={cn("w-72", optionsClassName)} input disabled={isDisabled || disabled} + placement="bottom-end" />
); diff --git a/apps/web/core/components/home/widgets/empty-states/no-projects.tsx b/apps/web/core/components/home/widgets/empty-states/no-projects.tsx index fe4384cb7e..09cb48f461 100644 --- a/apps/web/core/components/home/widgets/empty-states/no-projects.tsx +++ b/apps/web/core/components/home/widgets/empty-states/no-projects.tsx @@ -108,7 +108,7 @@ export const NoProjectsEmptyState = observer(function NoProjectsEmptyState() { flag: "visited_profile", cta: { text: "home.empty.personalize_account.cta", - link: `/${workspaceSlug}/settings/account`, + link: `/settings/profile/general`, disabled: false, }, }, diff --git a/apps/web/core/components/integration/delete-import-modal.tsx b/apps/web/core/components/integration/delete-import-modal.tsx deleted file mode 100644 index c09b0feac8..0000000000 --- a/apps/web/core/components/integration/delete-import-modal.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useState } from "react"; -import { useParams } from "next/navigation"; -import { mutate } from "swr"; -// icons -import { AlertTriangle } from "lucide-react"; -// services -import { Button } from "@plane/propel/button"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import type { IUser, IImporterService } from "@plane/types"; -import { Input, EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; -import { IMPORTER_SERVICES_LIST } from "@/constants/fetch-keys"; -import { IntegrationService } from "@/services/integrations/integration.service"; - -type Props = { - isOpen: boolean; - handleClose: () => void; - data: IImporterService | null; - user: IUser | null; -}; - -// services -const integrationService = new IntegrationService(); - -export function DeleteImportModal({ isOpen, handleClose, data }: Props) { - const [deleteLoading, setDeleteLoading] = useState(false); - const [confirmDeleteImport, setConfirmDeleteImport] = useState(false); - - const { workspaceSlug } = useParams(); - - const handleDeletion = () => { - if (!workspaceSlug || !data) return; - - setDeleteLoading(true); - - mutate( - IMPORTER_SERVICES_LIST(workspaceSlug), - (prevData) => (prevData ?? []).filter((i) => i.id !== data.id), - false - ); - - integrationService - .deleteImporterService(workspaceSlug, data.service, data.id) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Something went wrong. Please try again.", - }) - ) - .finally(() => { - setDeleteLoading(false); - handleClose(); - }); - }; - - if (!data) return <>; - - return ( - -
-
- - - -

Delete project

-
-
- -

- Are you sure you want to delete import from{" "} - {data?.service}? All of the data - related to the import will be permanently removed. This action cannot be undone. -

-
-
-

- To confirm, type delete import below: -

- { - if (e.target.value === "delete import") setConfirmDeleteImport(true); - else setConfirmDeleteImport(false); - }} - placeholder="Enter 'delete import'" - className="mt-2 w-full" - /> -
-
- - -
-
-
- ); -} diff --git a/apps/web/core/components/integration/github/auth.tsx b/apps/web/core/components/integration/github/auth.tsx deleted file mode 100644 index 60ed2e35fa..0000000000 --- a/apps/web/core/components/integration/github/auth.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { observer } from "mobx-react"; -// types -import { Button } from "@plane/propel/button"; -import type { IWorkspaceIntegration } from "@plane/types"; -// ui -// hooks -import { useInstance } from "@/hooks/store/use-instance"; -import useIntegrationPopup from "@/hooks/use-integration-popup"; - -type Props = { - workspaceIntegration: false | IWorkspaceIntegration | undefined; - provider: string | undefined; -}; - -export const GithubAuth = observer(function GithubAuth({ workspaceIntegration, provider }: Props) { - // store hooks - const { config } = useInstance(); - // hooks - const { startAuth, isConnecting } = useIntegrationPopup({ - provider, - github_app_name: config?.github_app_name || "", - slack_client_id: config?.slack_client_id || "", - }); - - return ( -
- {workspaceIntegration && workspaceIntegration?.id ? ( - - ) : ( - - )} -
- ); -}); diff --git a/apps/web/core/components/integration/github/import-configure.tsx b/apps/web/core/components/integration/github/import-configure.tsx deleted file mode 100644 index edcc316c6e..0000000000 --- a/apps/web/core/components/integration/github/import-configure.tsx +++ /dev/null @@ -1,50 +0,0 @@ -// components -import { Button } from "@plane/propel/button"; -import type { IAppIntegration, IWorkspaceIntegration } from "@plane/types"; -import type { TIntegrationSteps } from "@/components/integration"; -import { GithubAuth } from "@/components/integration"; -// types - -type Props = { - provider: string | undefined; - handleStepChange: (value: TIntegrationSteps) => void; - appIntegrations: IAppIntegration[] | undefined; - workspaceIntegrations: IWorkspaceIntegration[] | undefined; -}; - -export function GithubImportConfigure({ handleStepChange, provider, appIntegrations, workspaceIntegrations }: Props) { - // current integration from all the integrations available - const integration = - appIntegrations && appIntegrations.length > 0 && appIntegrations.find((i) => i.provider === provider); - - // current integration from workspace integrations - const workspaceIntegration = - integration && - workspaceIntegrations && - workspaceIntegrations.length > 0 && - workspaceIntegrations.find((i: any) => i.integration_detail.id === integration.id); - - return ( -
-
-
-
Configure
-
Set up your GitHub import.
-
-
- -
-
- -
- -
-
- ); -} diff --git a/apps/web/core/components/integration/github/import-confirm.tsx b/apps/web/core/components/integration/github/import-confirm.tsx deleted file mode 100644 index ba15e1a668..0000000000 --- a/apps/web/core/components/integration/github/import-confirm.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { FC } from "react"; - -// react-hook-form -import type { UseFormWatch } from "react-hook-form"; -// ui -import { Button } from "@plane/propel/button"; -// types -import type { TFormValues, TIntegrationSteps } from "@/components/integration"; - -type Props = { - handleStepChange: (value: TIntegrationSteps) => void; - watch: UseFormWatch; -}; - -export function GithubImportConfirm({ handleStepChange, watch }: Props) { - return ( -
-

- You are about to import work items from {watch("github").full_name}. Click on {'"'}Confirm & Import{'" '} - to complete the process. -

-
- - -
-
- ); -} diff --git a/apps/web/core/components/integration/github/import-data.tsx b/apps/web/core/components/integration/github/import-data.tsx deleted file mode 100644 index 83926e2d7f..0000000000 --- a/apps/web/core/components/integration/github/import-data.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import type { FC } from "react"; -import { observer } from "mobx-react"; -import type { Control, UseFormWatch } from "react-hook-form"; -import { Controller } from "react-hook-form"; -import { Button } from "@plane/propel/button"; -import type { IWorkspaceIntegration } from "@plane/types"; -// hooks -// components -import { CustomSearchSelect, ToggleSwitch } from "@plane/ui"; -import { truncateText } from "@plane/utils"; -import type { TFormValues, TIntegrationSteps } from "@/components/integration"; -import { SelectRepository } from "@/components/integration"; -// ui -// helpers -import { useProject } from "@/hooks/store/use-project"; -// types - -type Props = { - handleStepChange: (value: TIntegrationSteps) => void; - integration: IWorkspaceIntegration | false | undefined; - control: Control; - watch: UseFormWatch; -}; - -export const GithubImportData = observer(function GithubImportData(props: Props) { - const { handleStepChange, integration, control, watch } = props; - // store hooks - const { workspaceProjectIds, getProjectById } = useProject(); - - const options = workspaceProjectIds?.map((projectId) => { - const projectDetails = getProjectById(projectId); - - return { - value: `${projectDetails?.id}`, - query: `${projectDetails?.name}`, - content:

{truncateText(projectDetails?.name ?? "", 25)}

, - }; - }); - - return ( -
-
-
-
-

Select Repository

-

- Select the repository that you want the work items to be imported from. -

-
-
- {integration && ( - ( - Select Repository} - onChange={onChange} - characterLimit={50} - /> - )} - /> - )} -
-
-
-
-

Select Project

-

Select the project to import the work item to.

-
-
- {workspaceProjectIds && ( - ( - Select Project} - onChange={onChange} - options={options} - optionsClassName="w-48" - /> - )} - /> - )} -
-
-
-
-

Sync work item

-

Set whether you want to sync the work items or not.

-
-
- ( - onChange(!value)} /> - )} - /> -
-
-
-
- - -
-
- ); -}); diff --git a/apps/web/core/components/integration/github/import-users.tsx b/apps/web/core/components/integration/github/import-users.tsx deleted file mode 100644 index 0e53aa3d75..0000000000 --- a/apps/web/core/components/integration/github/import-users.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import type { FC } from "react"; - -// react-hook-form -import type { UseFormWatch } from "react-hook-form"; -// ui -import { Button } from "@plane/propel/button"; -// types -import type { IUserDetails, TFormValues, TIntegrationSteps } from "@/components/integration"; -import { SingleUserSelect } from "@/components/integration"; - -type Props = { - handleStepChange: (value: TIntegrationSteps) => void; - users: IUserDetails[]; - setUsers: React.Dispatch>; - watch: UseFormWatch; -}; - -export function GithubImportUsers({ handleStepChange, users, setUsers, watch }: Props) { - const isInvalid = users.filter((u) => u.import !== false && u.email === "").length > 0; - - return ( -
-
-
-
Name
-
Import as...
-
{users.filter((u) => u.import !== false).length} users selected
-
-
- {watch("collaborators").map((collaborator, index) => ( - - ))} -
-
-
- - -
-
- ); -} diff --git a/apps/web/core/components/integration/github/index.ts b/apps/web/core/components/integration/github/index.ts deleted file mode 100644 index c215e9a0c5..0000000000 --- a/apps/web/core/components/integration/github/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from "./auth"; -export * from "./import-configure"; -export * from "./import-confirm"; -export * from "./import-data"; -export * from "./import-users"; -export * from "./repo-details"; -export * from "./root"; -export * from "./select-repository"; -export * from "./single-user-select"; diff --git a/apps/web/core/components/integration/github/repo-details.tsx b/apps/web/core/components/integration/github/repo-details.tsx deleted file mode 100644 index a04474075c..0000000000 --- a/apps/web/core/components/integration/github/repo-details.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import type { FC } from "react"; -import { useEffect } from "react"; - -import { useParams } from "next/navigation"; - -// react-hook-form -import type { UseFormSetValue } from "react-hook-form"; -import useSWR from "swr"; -// services -// ui -import { Button } from "@plane/propel/button"; -import { Loader } from "@plane/ui"; -// types -import type { IUserDetails, TFormValues, TIntegrationSteps } from "@/components/integration"; -// fetch-keys -import { GITHUB_REPOSITORY_INFO } from "@/constants/fetch-keys"; -import { GithubIntegrationService } from "@/services/integrations"; - -type Props = { - selectedRepo: any; - handleStepChange: (value: TIntegrationSteps) => void; - setUsers: React.Dispatch>; - setValue: UseFormSetValue; -}; - -// services -const githubIntegrationService = new GithubIntegrationService(); - -export function GithubRepoDetails({ selectedRepo, handleStepChange, setUsers, setValue }: Props) { - const { workspaceSlug } = useParams(); - - const { data: repoInfo } = useSWR( - workspaceSlug && selectedRepo ? GITHUB_REPOSITORY_INFO(workspaceSlug, selectedRepo.name) : null, - workspaceSlug && selectedRepo - ? () => - githubIntegrationService.getGithubRepoInfo(workspaceSlug, { - owner: selectedRepo.owner.login, - repo: selectedRepo.name, - }) - : null - ); - - useEffect(() => { - if (!repoInfo) return; - - setValue("collaborators", repoInfo.collaborators); - - const fetchedUsers = repoInfo.collaborators.map((collaborator) => ({ - username: collaborator.login, - import: "map", - email: "", - })); - setUsers(fetchedUsers); - }, [repoInfo, setUsers, setValue]); - - return ( -
- {repoInfo ? ( - repoInfo.issue_count > 0 ? ( -
-
-
Repository Details
-
Import completed. We have found:
-
-
-
-

{repoInfo.issue_count}

-
Work items
-
-
-

{repoInfo.labels}

-
Labels
-
-
-

{repoInfo.collaborators.length}

-
Users
-
-
-
- ) : ( -
-
We didn{"'"}t find any work item in this repository.
-
- ) - ) : ( - - - - )} -
- - -
-
- ); -} diff --git a/apps/web/core/components/integration/github/root.tsx b/apps/web/core/components/integration/github/root.tsx deleted file mode 100644 index 4e144fe45d..0000000000 --- a/apps/web/core/components/integration/github/root.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import React, { useState } from "react"; -import Link from "next/link"; -import { useParams, useSearchParams } from "next/navigation"; -import { useForm } from "react-hook-form"; -import useSWR, { mutate } from "swr"; -import { ArrowLeft, List, Settings, UploadCloud } from "lucide-react"; -import { CheckIcon, MembersPropertyIcon } from "@plane/propel/icons"; -// types -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import type { IGithubRepoCollaborator, IGithubServiceImportFormData } from "@plane/types"; -// assets -import GithubLogo from "@/app/assets/services/github.png?url"; -// components -import { - GithubImportConfigure, - GithubImportData, - GithubRepoDetails, - GithubImportUsers, - GithubImportConfirm, -} from "@/components/integration"; -// fetch keys -import { APP_INTEGRATIONS, IMPORTER_SERVICES_LIST, WORKSPACE_INTEGRATIONS } from "@/constants/fetch-keys"; -// hooks -import { useAppRouter } from "@/hooks/use-app-router"; -// services -import { IntegrationService, GithubIntegrationService } from "@/services/integrations"; - -export type TIntegrationSteps = "import-configure" | "import-data" | "repo-details" | "import-users" | "import-confirm"; -export interface IIntegrationData { - state: TIntegrationSteps; -} - -export interface IUserDetails { - username: string; - import: any; - email: string; -} - -export type TFormValues = { - github: any; - project: string | null; - sync: boolean; - collaborators: IGithubRepoCollaborator[]; - users: IUserDetails[]; -}; - -const defaultFormValues = { - github: null, - project: null, - sync: false, -}; - -const integrationWorkflowData = [ - { - title: "Configure", - key: "import-configure", - icon: Settings, - }, - { - title: "Import Data", - key: "import-data", - icon: UploadCloud, - }, - { title: "Work item", key: "repo-details", icon: List }, - { - title: "Users", - key: "import-users", - icon: MembersPropertyIcon, - }, - { - title: "Confirm", - key: "import-confirm", - icon: CheckIcon, - }, -]; - -// services -const integrationService = new IntegrationService(); -const githubIntegrationService = new GithubIntegrationService(); - -export function GithubImporterRoot() { - const [currentStep, setCurrentStep] = useState({ - state: "import-configure", - }); - const [users, setUsers] = useState([]); - - const router = useAppRouter(); - const { workspaceSlug } = useParams(); - const searchParams = useSearchParams(); - const provider = searchParams.get("provider"); - - const { handleSubmit, control, setValue, watch } = useForm({ - defaultValues: defaultFormValues, - }); - - const { data: appIntegrations } = useSWR(APP_INTEGRATIONS, () => integrationService.getAppIntegrationsList()); - - const { data: workspaceIntegrations } = useSWR( - workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug) : null, - workspaceSlug ? () => integrationService.getWorkspaceIntegrationsList(workspaceSlug) : null - ); - - const activeIntegrationState = () => { - const currentElementIndex = integrationWorkflowData.findIndex((i) => i?.key === currentStep?.state); - - return currentElementIndex; - }; - - const handleStepChange = (value: TIntegrationSteps) => { - setCurrentStep((prevData) => ({ ...prevData, state: value })); - }; - - // current integration from all the integrations available - const integration = - appIntegrations && appIntegrations.length > 0 && appIntegrations.find((i) => i.provider === provider); - - // current integration from workspace integrations - const workspaceIntegration = - integration && workspaceIntegrations?.find((i: any) => i.integration_detail.id === integration.id); - - const createGithubImporterService = async (formData: TFormValues) => { - if (!formData.github || !formData.project) return; - - const payload: IGithubServiceImportFormData = { - metadata: { - owner: formData.github.owner.login, - name: formData.github.name, - repository_id: formData.github.id, - url: formData.github.html_url, - }, - data: { - users: users, - }, - config: { - sync: formData.sync, - }, - project_id: formData.project, - }; - - await githubIntegrationService - .createGithubServiceImport(workspaceSlug, payload) - .then(() => { - router.push(`/${workspaceSlug}/settings/imports`); - mutate(IMPORTER_SERVICES_LIST(workspaceSlug)); - }) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Import was unsuccessful. Please try again.", - }) - ); - }; - - return ( -
-
- - - -
Cancel import & go back
-
- - -
-
-
- GitHubLogo -
-
- {integrationWorkflowData.map((integration, index) => ( - -
- -
- {index < integrationWorkflowData.length - 1 && ( -
- {" "} -
- )} -
- ))} -
-
- -
-
- {currentStep?.state === "import-configure" && ( - - )} - {currentStep?.state === "import-data" && ( - - )} - {currentStep?.state === "repo-details" && ( - - )} - {currentStep?.state === "import-users" && ( - - )} - {currentStep?.state === "import-confirm" && ( - - )} -
-
-
-
-
- ); -} diff --git a/apps/web/core/components/integration/github/single-user-select.tsx b/apps/web/core/components/integration/github/single-user-select.tsx deleted file mode 100644 index 09b417f5e3..0000000000 --- a/apps/web/core/components/integration/github/single-user-select.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useParams } from "next/navigation"; -import useSWR from "swr"; -// plane types -import type { IGithubRepoCollaborator } from "@plane/types"; -// plane ui -import { Avatar, CustomSelect, CustomSearchSelect, Input } from "@plane/ui"; -// constants -import { getFileURL } from "@plane/utils"; -import { WORKSPACE_MEMBERS } from "@/constants/fetch-keys"; -// helpers -// plane web services -import { WorkspaceService } from "@/plane-web/services"; -// types -import type { IUserDetails } from "./root"; - -type Props = { - collaborator: IGithubRepoCollaborator; - index: number; - users: IUserDetails[]; - setUsers: React.Dispatch>; -}; - -const importOptions = [ - { - key: "map", - label: "Map to existing", - }, - { - key: "invite", - label: "Invite by email", - }, - { - key: false, - label: "Do not import", - }, -]; - -// services -const workspaceService = new WorkspaceService(); - -export function SingleUserSelect({ collaborator, index, users, setUsers }: Props) { - const { workspaceSlug } = useParams(); - - const { data: members } = useSWR( - workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug.toString()) : null, - workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug.toString()) : null - ); - - const options = members - ?.map((member) => { - if (!member?.member) return; - return { - value: member.member?.display_name, - query: member.member?.display_name ?? "", - content: ( -
- - {member.member?.display_name} -
- ), - }; - }) - .filter((member) => !!member) as - | { - value: string; - query: string; - content: React.ReactNode; - }[] - | undefined; - - return ( -
-
-
- {`${collaborator.login} -
-

{collaborator.login}

-
-
- {importOptions.find((o) => o.key === users[index].import)?.label}
} - onChange={(val: any) => { - const newUsers = [...users]; - newUsers[index].import = val; - newUsers[index].email = ""; - setUsers(newUsers); - }} - noChevron - > - {importOptions.map((option) => ( - -
{option.label}
-
- ))} - -
- {users[index].import === "invite" && ( - { - const newUsers = [...users]; - newUsers[index].email = e.target.value; - setUsers(newUsers); - }} - placeholder="Enter email of the user" - className="w-full py-1 text-11" - /> - )} - {users[index].import === "map" && members && ( - { - const newUsers = [...users]; - newUsers[index].email = val; - setUsers(newUsers); - }} - optionsClassName="w-48" - /> - )} -
- ); -} diff --git a/apps/web/core/components/integration/guide.tsx b/apps/web/core/components/integration/guide.tsx deleted file mode 100644 index eac78513f6..0000000000 --- a/apps/web/core/components/integration/guide.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { useParams, useSearchParams } from "next/navigation"; -import useSWR, { mutate } from "swr"; -// icons -import { RefreshCw } from "lucide-react"; -// plane imports -import { IMPORTERS_LIST } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -// types -import { Button } from "@plane/propel/button"; -import type { IImporterService } from "@plane/types"; -// assets -import GithubLogo from "@/app/assets/services/github.png?url"; -import JiraLogo from "@/app/assets/services/jira.svg?url"; -// components -import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "@/components/integration"; -import { ImportExportSettingsLoader } from "@/components/ui/loader/settings/import-and-export"; -// constants -import { IMPORTER_SERVICES_LIST } from "@/constants/fetch-keys"; -// hooks -import { useUser } from "@/hooks/store/user"; -// services -import { IntegrationService } from "@/services/integrations"; - -// services -const integrationService = new IntegrationService(); - -const getImporterLogo = (provider: string) => { - switch (provider) { - case "github": - return GithubLogo; - case "jira": - return JiraLogo; - default: - return ""; - } -}; - -// FIXME: [Deprecated] Remove this component -const IntegrationGuide = observer(function IntegrationGuide() { - // states - const [refreshing, setRefreshing] = useState(false); - const [deleteImportModal, setDeleteImportModal] = useState(false); - const [importToDelete, setImportToDelete] = useState(null); - // router - const { workspaceSlug } = useParams(); - const searchParams = useSearchParams(); - const provider = searchParams.get("provider"); - // store hooks - const { data: currentUser } = useUser(); - - const { t } = useTranslation(); - - const { data: importerServices } = useSWR( - workspaceSlug ? IMPORTER_SERVICES_LIST(workspaceSlug) : null, - workspaceSlug ? () => integrationService.getImporterServicesList(workspaceSlug) : null - ); - - const handleDeleteImport = (importService: IImporterService) => { - setImportToDelete(importService); - setDeleteImportModal(true); - }; - - return ( - <> - setDeleteImportModal(false)} - data={importToDelete} - user={currentUser || null} - /> -
- {(!provider || provider === "csv") && ( - <> - {/*
-
-
Relocation Guide
-
- You can now transfer all the work items that you{"'"}ve created in other tracking - services. This tool will guide you to relocate the work item to Plane. -
-
- -
- Read More - -
-
-
*/} - {IMPORTERS_LIST.map((service) => ( -
-
-
- {`${t(service.i18n_title)} -
-
-

{t(service.i18n_title)}

-

{t(service.i18n_description)}

-
-
-
- - - - - -
-
- ))} -
-
-

- Previous Imports - -

-
-
- {importerServices ? ( - importerServices.length > 0 ? ( -
-
- {importerServices.map((service) => ( - handleDeleteImport(service)} - /> - ))} -
-
- ) : ( -
- {/* */} -
- ) - ) : ( - - )} -
-
- - )} - - {provider && provider === "github" && } - {provider && provider === "jira" && } -
- - ); -}); - -export default IntegrationGuide; - -export { IntegrationGuide }; diff --git a/apps/web/core/components/integration/index.ts b/apps/web/core/components/integration/index.ts deleted file mode 100644 index a734113b6c..0000000000 --- a/apps/web/core/components/integration/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -// layout -export * from "./delete-import-modal"; -export * from "./guide"; -export * from "./single-import"; -export * from "./single-integration-card"; - -// github -export * from "./github"; -// jira -export * from "./jira"; -// slack -export * from "./slack"; diff --git a/apps/web/core/components/integration/jira/confirm-import.tsx b/apps/web/core/components/integration/jira/confirm-import.tsx deleted file mode 100644 index 15cc0da26b..0000000000 --- a/apps/web/core/components/integration/jira/confirm-import.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; - -// react hook form -import { useFormContext } from "react-hook-form"; -import type { IJiraImporterForm } from "@plane/types"; - -// types - -export function JiraConfirmImport() { - const { watch } = useFormContext(); - - return ( -
-
-
-

Confirm

-
- -
-

Migrating

-
-
-
-

{watch("data.total_issues")}

-

Work items

-
-
-

{watch("data.total_states")}

-

States

-
-
-

{watch("data.total_modules")}

-

Modules

-
-
-

{watch("data.total_labels")}

-

Labels

-
-
-

{watch("data.users").filter((user) => user.import).length}

-

User

-
-
-
-
- ); -} diff --git a/apps/web/core/components/integration/jira/give-details.tsx b/apps/web/core/components/integration/jira/give-details.tsx deleted file mode 100644 index 65601beecc..0000000000 --- a/apps/web/core/components/integration/jira/give-details.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import { observer } from "mobx-react"; -import Link from "next/link"; -import { useFormContext, Controller } from "react-hook-form"; -import { PlusIcon } from "@plane/propel/icons"; -import type { IJiraImporterForm } from "@plane/types"; -// hooks -// components -import { CustomSelect, Input } from "@plane/ui"; -// helpers -import { checkEmailValidity } from "@plane/utils"; -import { useCommandPalette } from "@/hooks/store/use-command-palette"; -import { useProject } from "@/hooks/store/use-project"; -// types - -export const JiraGetImportDetail = observer(function JiraGetImportDetail() { - // store hooks - const { toggleCreateProjectModal } = useCommandPalette(); - const { workspaceProjectIds, getProjectById } = useProject(); - // form info - const { - control, - formState: { errors }, - } = useFormContext(); - - return ( -
-
-
-

Jira Personal Access Token

-

- Get to know your access token by navigating to{" "} - - Atlassian Settings - -

-
- -
- ( - - )} - /> - {errors.metadata?.api_token && ( -

{errors.metadata.api_token.message}

- )} -
-
-
-
-

Jira Project Key

-

If XXX-123 is your work item, then enter XXX

-
-
- ( - - )} - /> - {errors.metadata?.project_key && ( -

{errors.metadata.project_key.message}

- )} -
-
-
-
-

Jira Email Address

-

Enter the Email account that you use in Jira account

-
-
- checkEmailValidity(value) || "Please enter a valid email address", - }} - render={({ field: { value, onChange, ref } }) => ( - - )} - /> - {errors.metadata?.email &&

{errors.metadata.email.message}

} -
-
-
-
-

Jira Installation or Cloud Host Name

-

Enter your companies cloud host name

-
-
- !/^https?:\/\//.test(value) || "Hostname should not begin with http:// or https://", - }} - render={({ field: { value, onChange, ref } }) => ( - - )} - /> - {errors.metadata?.cloud_hostname && ( -

{errors.metadata.cloud_hostname.message}

- )} -
-
-
-
-

Import to project

-

Select which project you want to import to.

-
-
- ( - - {value && value.trim() !== "" ? ( - getProjectById(value)?.name - ) : ( - Select a project - )} - - } - > - {workspaceProjectIds && workspaceProjectIds.length > 0 ? ( - workspaceProjectIds.map((projectId) => { - const projectDetails = getProjectById(projectId); - - if (!projectDetails) return; - - return ( - - {projectDetails.name} - - ); - }) - ) : ( -
-

You don{"'"}t have any project. Please create a project first.

-
- )} -
- -
-
- )} - /> -
-
-
- ); -}); diff --git a/apps/web/core/components/integration/jira/import-users.tsx b/apps/web/core/components/integration/jira/import-users.tsx deleted file mode 100644 index 9f64f53496..0000000000 --- a/apps/web/core/components/integration/jira/import-users.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import type { FC } from "react"; -import { useParams } from "next/navigation"; -import { useFormContext, useFieldArray, Controller } from "react-hook-form"; -import useSWR from "swr"; -// plane types -import type { IJiraImporterForm } from "@plane/types"; -// plane ui -import { Avatar, CustomSelect, CustomSearchSelect, Input, ToggleSwitch } from "@plane/ui"; -// constants -import { getFileURL } from "@plane/utils"; -import { WORKSPACE_MEMBERS } from "@/constants/fetch-keys"; -// helpers -// plane web services -import { WorkspaceService } from "@/plane-web/services"; - -const workspaceService = new WorkspaceService(); - -export function JiraImportUsers() { - const { workspaceSlug } = useParams(); - // form info - const { - control, - watch, - formState: { errors }, - } = useFormContext(); - - const { fields } = useFieldArray({ - control, - name: "data.users", - }); - - const { data: members } = useSWR( - workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug?.toString() ?? "") : null, - workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug?.toString() ?? "") : null - ); - - const options = members - ?.map((member) => { - if (!member?.member) return; - return { - value: member.member.email, - query: member.member.display_name ?? "", - content: ( -
- - {member.member.display_name} -
- ), - }; - }) - .filter((member) => !!member) as - | { - value: string; - query: string; - content: React.ReactNode; - }[] - | undefined; - - return ( -
-
-
-

Users

-

Update, invite or choose not to invite assignee

-
-
- } - /> -
-
- - {watch("data.invite_users") && ( -
-
-
Name
-
Import as
-
- -
- {fields.map((user, index) => ( -
-
-

{user.username}

-
-
- ( - {value ? value : ("Ignore" as any)}} - > - Invite by email - Map to existing - Do not import - - )} - /> -
-
- {watch(`data.users.${index}.import`) === "invite" && ( - ( - - )} - /> - )} - {watch(`data.users.${index}.import`) === "map" && ( - ( - - )} - /> - )} -
-
- ))} -
-
- )} -
- ); -} diff --git a/apps/web/core/components/integration/jira/index.ts b/apps/web/core/components/integration/jira/index.ts deleted file mode 100644 index bc22a82461..0000000000 --- a/apps/web/core/components/integration/jira/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -export * from "./root"; -export * from "./give-details"; -export * from "./jira-project-detail"; -export * from "./import-users"; -export * from "./confirm-import"; - -import type { IJiraImporterForm } from "@plane/types"; - -export type TJiraIntegrationSteps = - | "import-configure" - | "display-import-data" - | "select-import-data" - | "import-users" - | "import-confirmation"; - -export interface IJiraIntegrationData { - state: TJiraIntegrationSteps; -} - -export const jiraFormDefaultValues: IJiraImporterForm = { - metadata: { - cloud_hostname: "", - api_token: "", - project_key: "", - email: "", - }, - config: { - epics_to_modules: false, - }, - data: { - users: [], - invite_users: true, - total_issues: 0, - total_labels: 0, - total_modules: 0, - total_states: 0, - }, - project_id: "", -}; diff --git a/apps/web/core/components/integration/jira/jira-project-detail.tsx b/apps/web/core/components/integration/jira/jira-project-detail.tsx deleted file mode 100644 index 2fc79180b3..0000000000 --- a/apps/web/core/components/integration/jira/jira-project-detail.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React, { useEffect } from "react"; - -// next -import { useParams } from "next/navigation"; - -// swr -import { useFormContext, Controller } from "react-hook-form"; -import useSWR from "swr"; -import type { IJiraImporterForm, IJiraMetadata } from "@plane/types"; - -// react hook form - -// services -import { ToggleSwitch, Spinner } from "@plane/ui"; -import { JIRA_IMPORTER_DETAIL } from "@/constants/fetch-keys"; -import { JiraImporterService } from "@/services/integrations"; - -// fetch keys - -// components - -import type { IJiraIntegrationData, TJiraIntegrationSteps } from "."; - -type Props = { - setCurrentStep: React.Dispatch>; - setDisableTopBarAfter: React.Dispatch>; -}; - -// services -const jiraImporterService = new JiraImporterService(); - -export function JiraProjectDetail(props: Props) { - const { setCurrentStep, setDisableTopBarAfter } = props; - - const { - watch, - setValue, - control, - formState: { errors }, - } = useFormContext(); - - const { workspaceSlug } = useParams(); - - const params: IJiraMetadata = { - api_token: watch("metadata.api_token"), - project_key: watch("metadata.project_key"), - email: watch("metadata.email"), - cloud_hostname: watch("metadata.cloud_hostname"), - }; - - const { data: projectInfo, error } = useSWR( - workspaceSlug && - !errors.metadata?.api_token && - !errors.metadata?.project_key && - !errors.metadata?.email && - !errors.metadata?.cloud_hostname - ? JIRA_IMPORTER_DETAIL(workspaceSlug.toString(), params) - : null, - workspaceSlug && - !errors.metadata?.api_token && - !errors.metadata?.project_key && - !errors.metadata?.email && - !errors.metadata?.cloud_hostname - ? () => jiraImporterService.getJiraProjectInfo(workspaceSlug.toString(), params) - : null - ); - - useEffect(() => { - if (!projectInfo) return; - - setValue("data.total_issues", projectInfo.issues); - setValue("data.total_labels", projectInfo.labels); - setValue( - "data.users", - projectInfo.users?.map((user) => ({ - email: user.emailAddress, - import: false, - username: user.displayName, - })) - ); - setValue("data.total_states", projectInfo.states); - setValue("data.total_modules", projectInfo.modules); - }, [projectInfo, setValue]); - - useEffect(() => { - if (error) setDisableTopBarAfter("display-import-data"); - else setDisableTopBarAfter(null); - }, [error, setDisableTopBarAfter]); - - useEffect(() => { - if (!projectInfo && !error) setDisableTopBarAfter("display-import-data"); - else if (!error) setDisableTopBarAfter(null); - }, [projectInfo, error, setDisableTopBarAfter]); - - if (!projectInfo && !error) { - return ( -
- -
- ); - } - - if (error) { - return ( -
-

- Something went wrong. Please{" "} - {" "} - and check your Jira project details. -

-
- ); - } - - return ( -
-
-
-

Import Data

-

Import Completed. We have found:

-
-
-
-

{projectInfo?.issues}

-

Work items

-
-
-

{projectInfo?.states}

-

States

-
-
-

{projectInfo?.modules}

-

Modules

-
-
-

{projectInfo?.labels}

-

Labels

-
-
-

{projectInfo?.users?.length}

-

Users

-
-
-
- -
-
-

Import Epics

-

Import epics as modules

-
-
- } - /> -
-
-
- ); -} diff --git a/apps/web/core/components/integration/jira/root.tsx b/apps/web/core/components/integration/jira/root.tsx deleted file mode 100644 index 2941d5ff5e..0000000000 --- a/apps/web/core/components/integration/jira/root.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import React, { useState } from "react"; -import Link from "next/link"; -import { useParams } from "next/navigation"; -import { FormProvider, useForm } from "react-hook-form"; -import { mutate } from "swr"; -// icons -import { ArrowLeft, List, Settings } from "lucide-react"; -import { Button } from "@plane/propel/button"; -import { CheckIcon, MembersPropertyIcon } from "@plane/propel/icons"; -// types -import type { IJiraImporterForm } from "@plane/types"; -// assets -import JiraLogo from "@/app/assets/services/jira.svg?url"; -// fetch keys -import { IMPORTER_SERVICES_LIST } from "@/constants/fetch-keys"; -// hooks -import { useAppRouter } from "@/hooks/use-app-router"; -// services -import { JiraImporterService } from "@/services/integrations"; -// components -import type { TJiraIntegrationSteps, IJiraIntegrationData } from "."; -import { JiraGetImportDetail, JiraProjectDetail, JiraImportUsers, JiraConfirmImport, jiraFormDefaultValues } from "."; - -const integrationWorkflowData: Array<{ - title: string; - key: TJiraIntegrationSteps; - icon: any; -}> = [ - { - title: "Configure", - key: "import-configure", - icon: Settings, - }, - { - title: "Import Data", - key: "display-import-data", - icon: List, - }, - { - title: "Users", - key: "import-users", - icon: MembersPropertyIcon, - }, - { - title: "Confirm", - key: "import-confirmation", - icon: CheckIcon, - }, -]; - -// services -const jiraImporterService = new JiraImporterService(); - -export function JiraImporterRoot() { - const [currentStep, setCurrentStep] = useState({ - state: "import-configure", - }); - const [disableTopBarAfter, setDisableTopBarAfter] = useState(null); - - const router = useAppRouter(); - const { workspaceSlug } = useParams(); - - const methods = useForm({ - defaultValues: jiraFormDefaultValues, - mode: "all", - reValidateMode: "onChange", - }); - - const isValid = methods.formState.isValid; - - const onSubmit = async (data: IJiraImporterForm) => { - if (!workspaceSlug) return; - - await jiraImporterService - .createJiraImporter(workspaceSlug.toString(), data) - .then(() => { - mutate(IMPORTER_SERVICES_LIST(workspaceSlug.toString())); - router.push(`/${workspaceSlug}/settings/imports`); - }) - .catch((err) => { - console.error(err); - }); - }; - - const activeIntegrationState = () => { - const currentElementIndex = integrationWorkflowData.findIndex((i) => i?.key === currentStep?.state); - - return currentElementIndex; - }; - - return ( -
- - -
- -
-
Cancel import & go back
-
- - -
-
-
- jira logo -
-
- {integrationWorkflowData.map((integration, index) => ( - - - {index < integrationWorkflowData.length - 1 && ( -
- {" "} -
- )} -
- ))} -
-
- -
- -
-
- {currentStep.state === "import-configure" && } - {currentStep.state === "display-import-data" && ( - - )} - {currentStep?.state === "import-users" && } - {currentStep?.state === "import-confirmation" && } -
- -
- {currentStep?.state !== "import-configure" && ( - - )} - -
-
-
-
-
-
- ); -} diff --git a/apps/web/core/components/integration/single-import.tsx b/apps/web/core/components/integration/single-import.tsx deleted file mode 100644 index 43241ed2d6..0000000000 --- a/apps/web/core/components/integration/single-import.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { observer } from "mobx-react"; - -// plane imports -import { IMPORTERS_LIST } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { TrashIcon } from "@plane/propel/icons"; -import type { IImporterService } from "@plane/types"; -import { CustomMenu } from "@plane/ui"; -// icons -// helpers - -import { renderFormattedDate } from "@plane/utils"; -// types -// constants - -type Props = { - service: IImporterService; - refreshing: boolean; - handleDelete: () => void; -}; - -export const SingleImport = observer(function SingleImport({ service, refreshing, handleDelete }: Props) { - const { t } = useTranslation(); - - const importer = IMPORTERS_LIST.find((i) => i.provider === service.service); - return ( -
-
-

- {importer && ( - - Import from {t(importer.i18n_title)} to{" "} - - )} - {service.project_detail.name} - - {refreshing ? "Refreshing..." : service.status} - -

-
- {renderFormattedDate(service.created_at)}| - Imported by {service.initiated_by_detail?.display_name} -
-
- - - - - Delete import - - - -
- ); -}); diff --git a/apps/web/core/components/integration/slack/index.ts b/apps/web/core/components/integration/slack/index.ts deleted file mode 100644 index 4ea6cd1e4b..0000000000 --- a/apps/web/core/components/integration/slack/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./select-channel"; diff --git a/apps/web/core/components/labels/project-setting-label-list.tsx b/apps/web/core/components/labels/project-setting-label-list.tsx index 1791c003b9..2eb7dc488d 100644 --- a/apps/web/core/components/labels/project-setting-label-list.tsx +++ b/apps/web/core/components/labels/project-setting-label-list.tsx @@ -4,6 +4,7 @@ import { useParams } from "next/navigation"; // plane imports import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { Button } from "@plane/propel/button"; import { EmptyStateCompact } from "@plane/propel/empty-state"; import type { IIssueLabel } from "@plane/types"; import { Loader } from "@plane/ui"; @@ -76,16 +77,15 @@ export const ProjectSettingsLabelList = observer(function ProjectSettingsLabelLi { - newLabel(); - }, - }} - showButton={isEditable} + control={ + isEditable && ( + + ) + } /> - -
+
{showLabelForm && (
) : ( - projectLabelsTree && ( -
- {projectLabelsTree.map((label, index) => { - if (label.children && label.children.length) { - return ( - setSelectDeleteLabel(label)} - isUpdating={isUpdating} - setIsUpdating={setIsUpdating} - isLastChild={index === projectLabelsTree.length - 1} - onDrop={onDrop} - isEditable={isEditable} - labelOperationsCallbacks={labelOperationsCallbacks} - /> - ); - } - return ( - setSelectDeleteLabel(label)} - isChild={false} - isLastChild={index === projectLabelsTree.length - 1} - onDrop={onDrop} - isEditable={isEditable} - labelOperationsCallbacks={labelOperationsCallbacks} - /> - ); - })} -
- ) + projectLabelsTree?.map((label, index) => { + if (label.children && label.children.length) { + return ( + setSelectDeleteLabel(label)} + isUpdating={isUpdating} + setIsUpdating={setIsUpdating} + isLastChild={index === projectLabelsTree.length - 1} + onDrop={onDrop} + isEditable={isEditable} + labelOperationsCallbacks={labelOperationsCallbacks} + /> + ); + } + return ( + setSelectDeleteLabel(label)} + isChild={false} + isLastChild={index === projectLabelsTree.length - 1} + onDrop={onDrop} + isEditable={isEditable} + labelOperationsCallbacks={labelOperationsCallbacks} + /> + ); + }) ) ) : ( !showLabelForm && ( diff --git a/apps/web/core/components/navigation/app-rail-root.tsx b/apps/web/core/components/navigation/app-rail-root.tsx index bf096b59d3..eb37e7d212 100644 --- a/apps/web/core/components/navigation/app-rail-root.tsx +++ b/apps/web/core/components/navigation/app-rail-root.tsx @@ -17,13 +17,13 @@ import { AppSidebarItemsRoot } from "./items-root"; export const AppRailRoot = observer(() => { // router - const { workspaceSlug } = useParams(); + const { workspaceSlug, projectId } = useParams(); const pathname = usePathname(); // preferences const { preferences, updateDisplayMode } = useAppRailPreferences(); const { isCollapsed, toggleAppRail } = useAppRailVisibility(); - - const isSettingsPath = pathname.includes(`/${workspaceSlug}/settings`); + // derived values + const isWorkspaceSettingsPath = pathname.includes(`/${workspaceSlug}/settings`) && !projectId; const showLabel = preferences.displayMode === "icon_with_label"; const railWidth = showLabel ? "3.75rem" : "3rem"; @@ -52,7 +52,7 @@ export const AppRailRoot = observer(() => { label: "Settings", icon: , href: `/${workspaceSlug}/settings`, - isActive: isSettingsPath, + isActive: isWorkspaceSettingsPath, showLabel, }} /> diff --git a/apps/web/core/components/power-k/ui/pages/open-entity/project-settings-menu.tsx b/apps/web/core/components/power-k/ui/pages/open-entity/project-settings-menu.tsx index 109bb67ef8..25920d8d42 100644 --- a/apps/web/core/components/power-k/ui/pages/open-entity/project-settings-menu.tsx +++ b/apps/web/core/components/power-k/ui/pages/open-entity/project-settings-menu.tsx @@ -1,13 +1,13 @@ import { observer } from "mobx-react"; -// plane types -import { EUserPermissionsLevel } from "@plane/constants"; -// components +// plane imports +import { EUserPermissionsLevel, PROJECT_SETTINGS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +// components import type { TPowerKContext } from "@/components/power-k/core/types"; import { PowerKSettingsMenu } from "@/components/power-k/menus/settings"; +import { PROJECT_SETTINGS_ICONS } from "@/components/settings/project/sidebar/item-icon"; // hooks import { useUserPermissions } from "@/hooks/store/user"; -import { PROJECT_SETTINGS } from "@/plane-web/constants/project"; type Props = { context: TPowerKContext; @@ -35,7 +35,7 @@ export const PowerKOpenProjectSettingsMenu = observer(function PowerKOpenProject const settingsListWithIcons = settingsList.map((setting) => ({ ...setting, label: t(setting.i18n_label), - icon: setting.Icon, + icon: PROJECT_SETTINGS_ICONS[setting.key], })); return handleSelect(setting.href)} />; diff --git a/apps/web/core/components/power-k/ui/pages/open-entity/workspace-settings-menu.tsx b/apps/web/core/components/power-k/ui/pages/open-entity/workspace-settings-menu.tsx index 8fd3a50389..c4e9997ec5 100644 --- a/apps/web/core/components/power-k/ui/pages/open-entity/workspace-settings-menu.tsx +++ b/apps/web/core/components/power-k/ui/pages/open-entity/workspace-settings-menu.tsx @@ -5,10 +5,11 @@ import { EUserPermissionsLevel, WORKSPACE_SETTINGS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import type { TPowerKContext } from "@/components/power-k/core/types"; import { PowerKSettingsMenu } from "@/components/power-k/menus/settings"; +import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; // hooks import { useUserPermissions } from "@/hooks/store/user"; +// plane web imports import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper"; -import { WORKSPACE_SETTINGS_ICONS } from "app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/sidebar"; type Props = { context: TPowerKContext; @@ -31,7 +32,7 @@ export const PowerKOpenWorkspaceSettingsMenu = observer(function PowerKOpenWorks const settingsListWithIcons = settingsList.map((setting) => ({ ...setting, label: t(setting.i18n_label), - icon: WORKSPACE_SETTINGS_ICONS[setting.key as keyof typeof WORKSPACE_SETTINGS_ICONS], + icon: WORKSPACE_SETTINGS_ICONS[setting.key], })); return handleSelect(setting.href)} />; diff --git a/apps/web/core/components/preferences/list.tsx b/apps/web/core/components/preferences/list.tsx deleted file mode 100644 index 291eebf01e..0000000000 --- a/apps/web/core/components/preferences/list.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { PREFERENCE_OPTIONS } from "@plane/constants"; -import { PREFERENCE_COMPONENTS } from "@/plane-web/components/preferences/config"; - -export function PreferencesList() { - return ( -
- {PREFERENCE_OPTIONS.map((option) => { - const Component = PREFERENCE_COMPONENTS[option.id as keyof typeof PREFERENCE_COMPONENTS]; - return ; - })} -
- ); -} diff --git a/apps/web/core/components/preferences/section.tsx b/apps/web/core/components/preferences/section.tsx deleted file mode 100644 index f69c600bd2..0000000000 --- a/apps/web/core/components/preferences/section.tsx +++ /dev/null @@ -1,17 +0,0 @@ -interface SettingsSectionProps { - title: string; - description: string; - control: React.ReactNode; -} - -export function PreferencesSection({ title, description, control }: SettingsSectionProps) { - return ( -
-
-

{title}

-

{description}

-
-
{control}
-
- ); -} diff --git a/apps/web/core/components/profile/notification/email-notification-form.tsx b/apps/web/core/components/profile/notification/email-notification-form.tsx deleted file mode 100644 index 8ab4fac880..0000000000 --- a/apps/web/core/components/profile/notification/email-notification-form.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { useTranslation } from "@plane/i18n"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import type { IUserEmailNotificationSettings } from "@plane/types"; -// ui -import { ToggleSwitch } from "@plane/ui"; -// services -import { UserService } from "@/services/user.service"; -// types -interface IEmailNotificationFormProps { - data: IUserEmailNotificationSettings; -} - -// services -const userService = new UserService(); - -export function EmailNotificationForm(props: IEmailNotificationFormProps) { - const { data } = props; - const { t } = useTranslation(); - // form data - const { control, reset } = useForm({ - defaultValues: { - ...data, - }, - }); - - const handleSettingChange = async (key: keyof IUserEmailNotificationSettings, value: boolean) => { - try { - await userService.updateCurrentUserEmailNotificationSettings({ - [key]: value, - }); - setToast({ - title: t("success"), - type: TOAST_TYPE.SUCCESS, - message: t("email_notification_setting_updated_successfully"), - }); - } catch (_error) { - setToast({ - title: t("error"), - type: TOAST_TYPE.ERROR, - message: t("failed_to_update_email_notification_setting"), - }); - } - }; - - useEffect(() => { - reset(data); - }, [reset, data]); - - return ( - <> - {/* Notification Settings */} -
-
-
-
{t("property_changes")}
-
{t("property_changes_description")}
-
-
- ( - { - onChange(newValue); - handleSettingChange("property_change", newValue); - }} - size="sm" - /> - )} - /> -
-
-
-
-
{t("state_change")}
-
{t("state_change_description")}
-
-
- ( - { - onChange(newValue); - handleSettingChange("state_change", newValue); - }} - size="sm" - /> - )} - /> -
-
-
-
-
{t("issue_completed")}
-
{t("issue_completed_description")}
-
-
- ( - { - onChange(newValue); - handleSettingChange("issue_completed", newValue); - }} - size="sm" - /> - )} - /> -
-
-
-
-
{t("comments")}
-
{t("comments_description")}
-
-
- ( - { - onChange(newValue); - handleSettingChange("comment", newValue); - }} - size="sm" - /> - )} - /> -
-
-
-
-
{t("mentions")}
-
{t("mentions_description")}
-
-
- ( - { - onChange(newValue); - handleSettingChange("mention", newValue); - }} - size="sm" - /> - )} - /> -
-
-
- - ); -} diff --git a/apps/web/core/components/profile/preferences/language-timezone.tsx b/apps/web/core/components/profile/preferences/language-timezone.tsx deleted file mode 100644 index ef2de25298..0000000000 --- a/apps/web/core/components/profile/preferences/language-timezone.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { observer } from "mobx-react"; -import { SUPPORTED_LANGUAGES, useTranslation } from "@plane/i18n"; -import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import { CustomSelect } from "@plane/ui"; -import { TimezoneSelect } from "@/components/global"; -import { useUser, useUserProfile } from "@/hooks/store/user"; - -export const LanguageTimezone = observer(function LanguageTimezone() { - // store hooks - const { - data: user, - updateCurrentUser, - userProfile: { data: profile }, - } = useUser(); - const { updateUserProfile } = useUserProfile(); - const { t } = useTranslation(); - - const handleTimezoneChange = async (value: string) => { - try { - await updateCurrentUser({ user_timezone: value }); - setToast({ - title: "Success!", - message: "Timezone updated successfully", - type: TOAST_TYPE.SUCCESS, - }); - } catch (_error) { - setToast({ - title: "Error!", - message: "Failed to update timezone", - type: TOAST_TYPE.ERROR, - }); - } - }; - - const handleLanguageChange = async (value: string) => { - try { - await updateUserProfile({ language: value }); - setToast({ - title: "Success!", - message: "Language updated successfully", - type: TOAST_TYPE.SUCCESS, - }); - } catch (_error) { - setToast({ - title: "Error!", - message: "Failed to update language", - type: TOAST_TYPE.ERROR, - }); - } - }; - - const getLanguageLabel = (value: string) => { - const selectedLanguage = SUPPORTED_LANGUAGES.find((l) => l.value === value); - if (!selectedLanguage) return value; - return selectedLanguage.label; - }; - - return ( -
-
-
-
-
-

{t("timezone")} 

-

{t("timezone_setting")}

-
-
- -
-
-
-
-
-
-

{t("language")} 

-

{t("language_setting")}

-
-
- - {SUPPORTED_LANGUAGES.map((item) => ( - - {item.label} - - ))} - -
-
-
-
-
- ); -}); diff --git a/apps/web/core/components/profile/profile-setting-content-header.tsx b/apps/web/core/components/profile/profile-setting-content-header.tsx deleted file mode 100644 index 16e233a66f..0000000000 --- a/apps/web/core/components/profile/profile-setting-content-header.tsx +++ /dev/null @@ -1,14 +0,0 @@ -type Props = { - title: string; - description?: string; -}; - -export function ProfileSettingContentHeader(props: Props) { - const { title, description } = props; - return ( -
-
{title}
- {description &&
{description}
} -
- ); -} diff --git a/apps/web/core/components/profile/sidebar.tsx b/apps/web/core/components/profile/sidebar.tsx index 611fd6e53e..4d7633051e 100644 --- a/apps/web/core/components/profile/sidebar.tsx +++ b/apps/web/core/components/profile/sidebar.tsx @@ -1,26 +1,22 @@ import { useEffect, useRef } from "react"; import { observer } from "mobx-react"; -import Link from "next/link"; import { useParams } from "next/navigation"; -// icons - -// headless ui import { Disclosure, Transition } from "@headlessui/react"; -// plane helpers +// plane imports import { useOutsideClickDetector } from "@plane/hooks"; -// types import { useTranslation } from "@plane/i18n"; import { Logo } from "@plane/propel/emoji-icon-picker"; +import { IconButton } from "@plane/propel/icon-button"; import { EditIcon, ChevronDownIcon } from "@plane/propel/icons"; import { Tooltip } from "@plane/propel/tooltip"; import type { IUserProfileProjectSegregation } from "@plane/types"; -// plane ui import { Loader } from "@plane/ui"; import { cn, renderFormattedDate, getFileURL } from "@plane/utils"; // components import { CoverImage } from "@/components/common/cover-image"; // hooks import { useAppTheme } from "@/hooks/store/use-app-theme"; +import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useProject } from "@/hooks/store/use-project"; import { useUser } from "@/hooks/store/user"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -37,11 +33,12 @@ export const ProfileSidebar = observer(function ProfileSidebar(props: TProfileSi // refs const ref = useRef(null); // router - const { userId, workspaceSlug } = useParams(); + const { userId } = useParams(); // store hooks const { data: currentUser } = useUser(); const { profileSidebarCollapsed, toggleProfileSidebar } = useAppTheme(); const { getProjectById } = useProject(); + const { toggleProfileSettingsModal } = useCommandPalette(); const { isMobile } = usePlatformOS(); const { t } = useTranslation(); // derived values @@ -84,7 +81,7 @@ export const ProfileSidebar = observer(function ProfileSidebar(props: TProfileSi return (
{currentUser?.id === userId && ( -
- - - - - +
+ + toggleProfileSettingsModal({ + activeTab: "general", + isOpen: true, + }) + } + />
)} START_OF_THE_WEEK_OPTIONS.find((option) => option.value === startOfWeek)?.label; @@ -27,27 +28,27 @@ export const StartOfWeekPreference = observer(function StartOfWeekPreference(pro }; return ( - - - <> - {START_OF_THE_WEEK_OPTIONS.map((day) => ( - - {day.label} - - ))} - - -
+ + <> + {START_OF_THE_WEEK_OPTIONS.map((day) => ( + + {day.label} + + ))} + + } /> ); diff --git a/apps/web/core/components/project-states/root.tsx b/apps/web/core/components/project-states/root.tsx index d9dbfc9761..1143b268b4 100644 --- a/apps/web/core/components/project-states/root.tsx +++ b/apps/web/core/components/project-states/root.tsx @@ -62,13 +62,11 @@ export const ProjectStateRoot = observer(function ProjectStateRoot(props: TProje if (!groupedProjectStates) return ; return ( -
- -
+ ); }); diff --git a/apps/web/core/components/project/settings/archive-project/archive-restore-modal.tsx b/apps/web/core/components/project/archive-restore-modal.tsx similarity index 100% rename from apps/web/core/components/project/settings/archive-project/archive-restore-modal.tsx rename to apps/web/core/components/project/archive-restore-modal.tsx diff --git a/apps/web/core/components/project/card.tsx b/apps/web/core/components/project/card.tsx index d4ee4bc905..9c7dcd7b56 100644 --- a/apps/web/core/components/project/card.tsx +++ b/apps/web/core/components/project/card.tsx @@ -26,7 +26,7 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; import { CoverImage } from "@/components/common/cover-image"; import { DeleteProjectModal } from "./delete-project-modal"; import { JoinProjectModal } from "./join-project-modal"; -import { ArchiveRestoreProjectModal } from "./settings/archive-project/archive-restore-modal"; +import { ArchiveRestoreProjectModal } from "./archive-restore-modal"; type Props = { project: IProject; diff --git a/apps/web/core/components/project/form.tsx b/apps/web/core/components/project/form.tsx index 634275c40f..527dab3224 100644 --- a/apps/web/core/components/project/form.tsx +++ b/apps/web/core/components/project/form.tsx @@ -258,7 +258,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
-
+

{t("common.project_name")}

void; -} - -export function ArchiveProjectSelection(props: IArchiveProject) { - const { projectDetails, handleArchive } = props; - - return ( - - {({ open }) => ( -
- - Archive project - {open ? : } - - - -
- - Archiving a project will unlist your project from your side navigation although you will still be able - to access it from your projects page. You can restore the project or delete it whenever you want. - -
- {projectDetails ? ( -
- -
- ) : ( - - - - )} -
-
-
-
-
- )} -
- ); -} diff --git a/apps/web/core/components/project/settings/control-section.tsx b/apps/web/core/components/project/settings/control-section.tsx new file mode 100644 index 0000000000..9f850aa202 --- /dev/null +++ b/apps/web/core/components/project/settings/control-section.tsx @@ -0,0 +1,82 @@ +import { useState } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "react-router"; +// plane imports +import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Button } from "@plane/propel/button"; +// components +import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item"; +// hooks +import { useProject } from "@/hooks/store/use-project"; +// local imports +import { ArchiveRestoreProjectModal } from "../archive-restore-modal"; +import { DeleteProjectModal } from "../delete-project-modal"; + +type Props = { + projectId: string; +}; + +export const GeneralProjectSettingsControlSection = observer(function GeneralProjectSettingsControlSection( + props: Props +) { + const { projectId } = props; + // states + const [selectProject, setSelectedProject] = useState(null); + const [archiveProject, setArchiveProject] = useState(false); + // params + const { workspaceSlug } = useParams(); + // store hooks + const { currentProjectDetails } = useProject(); + // translation + const { t } = useTranslation(); + + if (!currentProjectDetails) return null; + + return ( +
+ {workspaceSlug && ( + setArchiveProject(false)} + archive + /> + )} + setSelectedProject(null)} + /> +
+ {/* Project Selector */} + setArchiveProject(true)}> + {t("archive")} + + } + /> + {/* Format Selector */} + setSelectedProject(currentProjectDetails.id ?? null)} + data-ph-element={PROJECT_TRACKER_ELEMENTS.DELETE_PROJECT_BUTTON} + > + {t("delete")} + + } + /> +
+
+ ); +}); diff --git a/apps/web/core/components/project/settings/delete-project-section.tsx b/apps/web/core/components/project/settings/delete-project-section.tsx deleted file mode 100644 index 1d81304301..0000000000 --- a/apps/web/core/components/project/settings/delete-project-section.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from "react"; -import { Disclosure, Transition } from "@headlessui/react"; -// types -import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; -import { Button } from "@plane/propel/button"; -import { ChevronRightIcon, ChevronUpIcon } from "@plane/propel/icons"; -import type { IProject } from "@plane/types"; -// ui -import { Loader } from "@plane/ui"; - -export interface IDeleteProjectSection { - projectDetails: IProject; - handleDelete: () => void; -} - -export function DeleteProjectSection(props: IDeleteProjectSection) { - const { projectDetails, handleDelete } = props; - - return ( - - {({ open }) => ( -
- - Delete project - {open ? : } - - - - -
- - When deleting a project, all of the data and resources within that project will be permanently removed - and cannot be recovered. - -
- {projectDetails ? ( -
- -
- ) : ( - - - - )} -
-
-
-
-
- )} -
- ); -} diff --git a/apps/web/core/components/project/settings/features-list.tsx b/apps/web/core/components/project/settings/features-list.tsx index fcfab32ae0..655cb7e39c 100644 --- a/apps/web/core/components/project/settings/features-list.tsx +++ b/apps/web/core/components/project/settings/features-list.tsx @@ -5,13 +5,14 @@ import { setPromiseToast } from "@plane/propel/toast"; import { Tooltip } from "@plane/propel/tooltip"; import type { IProject } from "@plane/types"; // components +import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item"; import { SettingsHeading } from "@/components/settings/heading"; // hooks import { useProject } from "@/hooks/store/use-project"; -import { useUser } from "@/hooks/store/user"; // plane web imports import { UpgradeBadge } from "@/plane-web/components/workspace/upgrade-badge"; import { PROJECT_FEATURES_LIST } from "@/plane-web/constants/project/settings"; +// local imports import { ProjectFeatureToggle } from "./helper"; type Props = { @@ -24,12 +25,11 @@ export const ProjectFeaturesList = observer(function ProjectFeaturesList(props: const { workspaceSlug, projectId, isAdmin } = props; // store hooks const { t } = useTranslation(); - const { data: currentUser } = useUser(); const { getProjectById, updateProject } = useProject(); // derived values const currentProjectDetails = getProjectById(projectId); - const handleSubmit = (featureKey: string, featureProperty: string) => { + const handleSubmit = (_featureKey: string, featureProperty: string) => { if (!workspaceSlug || !projectId || !currentProjectDetails) return; // making the request to update the project feature @@ -54,49 +54,45 @@ export const ProjectFeaturesList = observer(function ProjectFeaturesList(props: }); }; - if (!currentUser) return <>; - return ( -
+ <> {Object.entries(PROJECT_FEATURES_LIST).map(([featureSectionKey, feature]) => ( -
+
- {Object.entries(feature.featureList).map(([featureItemKey, featureItem]) => ( -
-
-
-
{featureItem.icon}
-
-
-

{t(featureItem.key)}

+
+ {Object.entries(feature.featureList).map(([featureItemKey, featureItem]) => ( +
+ + {t(featureItem.key)} {featureItem.isPro && ( )} -
-

- {t(`${featureItem.key}_description`)} -

-
-
- + } + description={t(`${featureItem.key}_description`)} + control={ + + } /> + {currentProjectDetails?.[featureItem.property as keyof IProject] && ( +
{featureItem.renderChildren?.(currentProjectDetails, workspaceSlug)}
+ )}
-
- {currentProjectDetails?.[featureItem.property as keyof IProject] && - featureItem.renderChildren?.(currentProjectDetails, workspaceSlug)} -
-
- ))} + ))} +
))} -
+ ); }); diff --git a/apps/web/core/components/settings/boxed-control-item.tsx b/apps/web/core/components/settings/boxed-control-item.tsx new file mode 100644 index 0000000000..6aac41f6ee --- /dev/null +++ b/apps/web/core/components/settings/boxed-control-item.tsx @@ -0,0 +1,28 @@ +// plane imports +import { cn } from "@plane/utils"; + +type Props = { + className?: string; + control?: React.ReactNode; + description?: React.ReactNode; + title: React.ReactNode; +}; + +export function SettingsBoxedControlItem(props: Props) { + const { className, control, description, title } = props; + + return ( +
+
+

{title}

+ {description &&

{description}

} +
+ {control &&
{control}
} +
+ ); +} diff --git a/apps/web/core/components/settings/content-wrapper.tsx b/apps/web/core/components/settings/content-wrapper.tsx index a15566d666..9af0f97a4c 100644 --- a/apps/web/core/components/settings/content-wrapper.tsx +++ b/apps/web/core/components/settings/content-wrapper.tsx @@ -1,22 +1,35 @@ -import type { ReactNode } from "react"; -import { observer } from "mobx-react"; +// plane imports +import { ScrollArea } from "@plane/propel/scrollarea"; import { cn } from "@plane/utils"; +// components +import { AppHeader } from "@/components/core/app-header"; -type TProps = { - children: ReactNode; - size?: "lg" | "md"; +type Props = { + children: React.ReactNode; + header?: React.ReactNode; + hugging?: boolean; }; -export const SettingsContentWrapper = observer(function SettingsContentWrapper(props: TProps) { - const { children, size = "md" } = props; + +export function SettingsContentWrapper(props: Props) { + const { children, header, hugging = false } = props; return ( -
-
{children}
+
+ {header && ( +
+ +
+ )} + +
+ {children} +
+
); -}); +} diff --git a/apps/web/core/components/settings/control-item.tsx b/apps/web/core/components/settings/control-item.tsx new file mode 100644 index 0000000000..b6074ed86d --- /dev/null +++ b/apps/web/core/components/settings/control-item.tsx @@ -0,0 +1,19 @@ +type Props = { + control: React.ReactNode; + description: string; + title: React.ReactNode; +}; + +export function SettingsControlItem(props: Props) { + const { control, description, title } = props; + + return ( +
+
+

{title}

+

{description}

+
+
{control}
+
+ ); +} diff --git a/apps/web/core/components/settings/header.tsx b/apps/web/core/components/settings/header.tsx deleted file mode 100644 index b62ad4d4b1..0000000000 --- a/apps/web/core/components/settings/header.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { observer } from "mobx-react"; -import Link from "next/link"; -import { ChevronLeftIcon } from "lucide-react"; -// plane imports -import { useTranslation } from "@plane/i18n"; -import { getButtonStyling } from "@plane/propel/button"; -import { cn } from "@plane/utils"; -// hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; -import { useUserSettings } from "@/hooks/store/user"; -// local imports -import { WorkspaceLogo } from "../workspace/logo"; -import SettingsTabs from "./tabs"; - -export const SettingsHeader = observer(function SettingsHeader() { - // hooks - const { t } = useTranslation(); - const { currentWorkspace } = useWorkspace(); - const { isScrolled } = useUserSettings(); - - return ( -
- - - - {/* Breadcrumb */} - - -
-
{t("back_to_workspace")}
- {/* Last workspace */} -
- -
{currentWorkspace?.name}
-
-
- -
- {/* Description */} -
{t("settings")}
- {/* Actions */} - -
-
- ); -}); diff --git a/apps/web/core/components/settings/heading.tsx b/apps/web/core/components/settings/heading.tsx index 9f8949ee5a..7cd44034fc 100644 --- a/apps/web/core/components/settings/heading.tsx +++ b/apps/web/core/components/settings/heading.tsx @@ -1,48 +1,32 @@ -import { Button } from "@plane/propel/button"; +// plane imports import { cn } from "@plane/ui"; type Props = { - title: string | React.ReactNode; - description?: string; - appendToRight?: React.ReactNode; - showButton?: boolean; - customButton?: React.ReactNode; - button?: { - label: string; - onClick: () => void; - }; className?: string; + control?: React.ReactNode; + description?: React.ReactNode; + title?: React.ReactNode; + variant?: "h3" | "h4" | "h6"; }; -export function SettingsHeading({ - title, - description, - button, - appendToRight, - customButton, - showButton = true, - className, -}: Props) { +export function SettingsHeading({ className, control, description, title, variant = "h3" }: Props) { return ( -
+
- {typeof title === "string" ?

{title}

: title} - {description &&
{description}
} + {title && ( +

+ {title} +

+ )} + {description &&

{description}

}
- {showButton && customButton} - {button && showButton && ( - - )} - {appendToRight} + {control}
); } - -export default SettingsHeading; diff --git a/apps/web/core/components/settings/helper.ts b/apps/web/core/components/settings/helper.ts index e621fcdbd6..71fe03f2f1 100644 --- a/apps/web/core/components/settings/helper.ts +++ b/apps/web/core/components/settings/helper.ts @@ -1,5 +1,4 @@ -import { GROUPED_PROFILE_SETTINGS, GROUPED_WORKSPACE_SETTINGS } from "@plane/constants"; -import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project"; +import { GROUPED_WORKSPACE_SETTINGS, PROJECT_SETTINGS_FLAT_MAP } from "@plane/constants"; const hrefToLabelMap = (options: Record>) => Object.values(options) @@ -14,9 +13,7 @@ const hrefToLabelMap = (options: Record { acc[setting.href] = setting.i18n_label; return acc; @@ -39,14 +36,6 @@ export const getWorkspaceActivePath = (pathname: string) => { return workspaceHrefToLabelMap[subPath]; }; -export const getProfileActivePath = (pathname: string) => { - const parts = pathname.split("/").filter(Boolean); - const settingsIndex = parts.indexOf("settings"); - if (settingsIndex === -1) return null; - const subPath = "/" + parts.slice(settingsIndex, settingsIndex + 3).join("/"); - return profiletHrefToLabelMap[subPath]; -}; - export const getProjectActivePath = (pathname: string) => { const parts = pathname.split("/").filter(Boolean); const settingsIndex = parts.indexOf("settings"); diff --git a/apps/web/core/components/settings/mobile/index.ts b/apps/web/core/components/settings/mobile/index.ts deleted file mode 100644 index 870d3745cd..0000000000 --- a/apps/web/core/components/settings/mobile/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./nav"; diff --git a/apps/web/core/components/settings/mobile/nav.tsx b/apps/web/core/components/settings/mobile/nav.tsx index ea5871eca3..ac8e2fa2a5 100644 --- a/apps/web/core/components/settings/mobile/nav.tsx +++ b/apps/web/core/components/settings/mobile/nav.tsx @@ -5,9 +5,10 @@ import { useOutsideClickDetector } from "@plane/hooks"; import { useTranslation } from "@plane/i18n"; import { ChevronRightIcon } from "@plane/propel/icons"; import { useUserSettings } from "@/hooks/store/user"; +import { IconButton } from "@plane/propel/icon-button"; type Props = { - hamburgerContent: React.ComponentType<{ isMobile: boolean }>; + hamburgerContent: React.ComponentType<{ className?: string; isMobile?: boolean }>; activePath: string; }; @@ -24,23 +25,19 @@ export const SettingsMobileNav = observer(function SettingsMobileNav(props: Prop }); return ( -
-
-
- {!sidebarCollapsed && } - -
- {/* path */} -
- - {t(activePath)} -
+
+
+ {!sidebarCollapsed && ( +
+ +
+ )} + toggleSidebar()} /> +
+ {/* path */} +
+ + {t(activePath)}
); diff --git a/apps/web/core/components/settings/page-header.tsx b/apps/web/core/components/settings/page-header.tsx new file mode 100644 index 0000000000..56bb7dd6ca --- /dev/null +++ b/apps/web/core/components/settings/page-header.tsx @@ -0,0 +1,17 @@ +import { Header } from "@plane/ui"; + +type Props = { + leftItem?: React.ReactNode; + rightItem?: React.ReactNode; +}; + +export function SettingsPageHeader(props: Props) { + const { leftItem, rightItem } = props; + + return ( +
+ {leftItem && {leftItem}} + {rightItem && {rightItem}} +
+ ); +} diff --git a/apps/web/core/components/settings/sidebar/index.ts b/apps/web/core/components/settings/profile/content/index.ts similarity index 100% rename from apps/web/core/components/settings/sidebar/index.ts rename to apps/web/core/components/settings/profile/content/index.ts diff --git a/apps/web/core/components/settings/profile/content/pages/activity/activity-list.tsx b/apps/web/core/components/settings/profile/content/pages/activity/activity-list.tsx new file mode 100644 index 0000000000..43798f71ce --- /dev/null +++ b/apps/web/core/components/settings/profile/content/pages/activity/activity-list.tsx @@ -0,0 +1,188 @@ +import { useEffect } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import useSWR from "swr"; +// icons +import { History, MessageSquare } from "lucide-react"; +import { calculateTimeAgo, getFileURL } from "@plane/utils"; +// hooks +import { ActivityIcon, ActivityMessage } from "@/components/core/activity"; +import { RichTextEditor } from "@/components/editor/rich-text"; +import { ActivitySettingsLoader } from "@/components/ui/loader/settings/activity"; +// constants +import { USER_ACTIVITY } from "@/constants/fetch-keys"; +// hooks +import { useUserProfile } from "@/hooks/store/user/user-user-profile"; +// services +import { UserService } from "@/services/user.service"; +const userService = new UserService(); + +type Props = { + cursor: string; + perPage: number; + updateResultsCount: (count: number) => void; + updateTotalPages: (count: number) => void; + updateEmptyState: (state: boolean) => void; +}; + +export const ActivityProfileSettingsList = observer(function ProfileActivityListPage(props: Props) { + const { cursor, perPage, updateResultsCount, updateTotalPages, updateEmptyState } = props; + // store hooks + const { data: currentUser } = useUserProfile(); + + const { data: userProfileActivity } = useSWR( + USER_ACTIVITY({ + cursor, + }), + () => + userService.getUserActivity({ + cursor, + per_page: perPage, + }) + ); + + useEffect(() => { + if (!userProfileActivity) return; + + // if no results found then show empty state + if (userProfileActivity.total_results === 0) updateEmptyState(true); + + updateTotalPages(userProfileActivity.total_pages); + updateResultsCount(userProfileActivity.results.length); + }, [updateResultsCount, updateTotalPages, userProfileActivity, updateEmptyState]); + + // TODO: refactor this component + return ( + <> + {userProfileActivity ? ( +
    + {userProfileActivity.results.map((activityItem: any) => { + if (activityItem.field === "comment") + return ( +
    +
    +
    + {activityItem.field ? ( + activityItem.new_value === "restore" && + ) : activityItem.actor_detail.avatar_url && activityItem.actor_detail.avatar_url !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
    + {activityItem.actor_detail.display_name?.[0]} +
    + )} + + + +
    +
    +
    +
    + {activityItem.actor_detail.is_bot + ? activityItem.actor_detail.first_name + " Bot" + : activityItem.actor_detail.display_name} +
    +

    + Commented {calculateTimeAgo(activityItem.created_at)} +

    +
    +
    + +
    +
    +
    +
    + ); + + const message = ; + + if ("field" in activityItem && activityItem.field !== "updated_by") + return ( +
  • +
    +
    + <> +
    +
    +
    +
    + {activityItem.field ? ( + activityItem.new_value === "restore" ? ( + + ) : ( + + ) + ) : activityItem.actor_detail.avatar_url && + activityItem.actor_detail.avatar_url !== "" ? ( + {activityItem.actor_detail.display_name} + ) : ( +
    + {activityItem.actor_detail.display_name?.[0]} +
    + )} +
    +
    +
    +
    +
    +
    + {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( + Plane + ) : activityItem.actor_detail.is_bot ? ( + {activityItem.actor_detail.first_name} Bot + ) : ( + + + {currentUser?.id === activityItem.actor_detail.id + ? "You" + : activityItem.actor_detail.display_name} + + + )}{" "} +
    + {message}{" "} + + {calculateTimeAgo(activityItem.created_at)} + +
    +
    +
    + +
    +
    +
  • + ); + })} +
+ ) : ( + + )} + + ); +}); diff --git a/apps/web/core/components/settings/profile/content/pages/activity/index.ts b/apps/web/core/components/settings/profile/content/pages/activity/index.ts new file mode 100644 index 0000000000..1efe34c51e --- /dev/null +++ b/apps/web/core/components/settings/profile/content/pages/activity/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/activity/page.tsx b/apps/web/core/components/settings/profile/content/pages/activity/root.tsx similarity index 81% rename from apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/activity/page.tsx rename to apps/web/core/components/settings/profile/content/pages/activity/root.tsx index 00b795dfdc..71c3292a32 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/activity/page.tsx +++ b/apps/web/core/components/settings/profile/content/pages/activity/root.tsx @@ -1,23 +1,22 @@ import { useState } from "react"; +import { ChevronDown } from "lucide-react"; import { observer } from "mobx-react"; import { useTheme } from "next-themes"; +// plane imports import { useTranslation } from "@plane/i18n"; -// ui import { Button } from "@plane/propel/button"; // assets import darkActivityAsset from "@/app/assets/empty-state/profile/activity-dark.webp?url"; import lightActivityAsset from "@/app/assets/empty-state/profile/activity-light.webp?url"; // components -import { PageHead } from "@/components/core/page-title"; import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root"; -import { ProfileActivityListPage } from "@/components/profile/activity/profile-activity-list"; -// hooks -import { SettingsHeading } from "@/components/settings/heading"; -import { ChevronDown } from "lucide-react"; +import { ProfileSettingsHeading } from "@/components/settings/profile/heading"; +// local imports +import { ActivityProfileSettingsList } from "./activity-list"; const PER_PAGE = 100; -function ProfileActivityPage() { +export const ActivityProfileSettings = observer(function ActivityProfileSettings() { // states const [pageCount, setPageCount] = useState(1); const [totalPages, setTotalPages] = useState(0); @@ -41,7 +40,7 @@ function ProfileActivityPage() { const activityPages: React.ReactNode[] = []; for (let i = 0; i < pageCount; i++) activityPages.push( - - + @@ -72,13 +71,12 @@ function ProfileActivityPage() { } return ( - <> - - + -
{activityPages}
+
{activityPages}
{isLoadMoreVisible && (
)} - +
); -} - -export default observer(ProfileActivityPage); +}); diff --git a/apps/web/core/components/settings/profile/content/pages/api-tokens.tsx b/apps/web/core/components/settings/profile/content/pages/api-tokens.tsx new file mode 100644 index 0000000000..630b0ab5ff --- /dev/null +++ b/apps/web/core/components/settings/profile/content/pages/api-tokens.tsx @@ -0,0 +1,73 @@ +import { useState } from "react"; +import { observer } from "mobx-react"; +import useSWR from "swr"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { Button } from "@plane/propel/button"; +import { EmptyStateCompact } from "@plane/propel/empty-state"; +import { APITokenService } from "@plane/services"; +// components +import { CreateApiTokenModal } from "@/components/api-token/modal/create-token-modal"; +import { ApiTokenListItem } from "@/components/api-token/token-list-item"; +import { ProfileSettingsHeading } from "@/components/settings/profile/heading"; +import { APITokenSettingsLoader } from "@/components/ui/loader/settings/api-token"; +// constants +import { API_TOKENS_LIST } from "@/constants/fetch-keys"; + +const apiTokenService = new APITokenService(); + +export const APITokensProfileSettings = observer(function APITokensProfileSettings() { + // states + const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false); + // store hooks + const { data: tokens } = useSWR(API_TOKENS_LIST, () => apiTokenService.list()); + // translation + const { t } = useTranslation(); + + if (!tokens) { + return ; + } + + return ( +
+ setIsCreateTokenModalOpen(false)} /> + setIsCreateTokenModalOpen(true)}> + {t("workspace_settings.settings.api_tokens.add_token")} + + } + /> +
+ {tokens.length > 0 ? ( + <> +
+ {tokens.map((token) => ( + + ))} +
+ + ) : ( + { + setIsCreateTokenModalOpen(true); + }, + }, + ]} + align="start" + rootClassName="py-20" + /> + )} +
+
+ ); +}); diff --git a/apps/web/core/components/profile/form.tsx b/apps/web/core/components/settings/profile/content/pages/general/form.tsx similarity index 88% rename from apps/web/core/components/profile/form.tsx rename to apps/web/core/components/settings/profile/content/pages/general/form.tsx index ec9d52bb29..8d47033da3 100644 --- a/apps/web/core/components/profile/form.tsx +++ b/apps/web/core/components/settings/profile/content/pages/general/form.tsx @@ -2,11 +2,9 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; import { CircleUserRound } from "lucide-react"; -import { Disclosure, Transition } from "@headlessui/react"; // plane imports import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; -import { ChevronDownIcon } from "@plane/propel/icons"; import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/propel/toast"; import { EFileAssetType } from "@plane/types"; import type { IUser, TUserProfile } from "@plane/types"; @@ -18,6 +16,7 @@ import { ImagePickerPopover } from "@/components/core/image-picker-popover"; import { ChangeEmailModal } from "@/components/core/modals/change-email-modal"; import { UserImageUploadModal } from "@/components/core/modals/user-image-upload-modal"; import { CoverImage } from "@/components/common/cover-image"; +import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item"; // helpers import { handleCoverImageChange } from "@/helpers/cover-image.helper"; // hooks @@ -38,12 +37,12 @@ type TUserProfileForm = { user_timezone: string; }; -export type TProfileFormProps = { +type Props = { user: IUser; profile: TUserProfile; }; -export const ProfileForm = observer(function ProfileForm(props: TProfileFormProps) { +export const GeneralProfileSettingsForm = observer(function GeneralProfileSettingsForm(props: Props) { const { user, profile } = props; // states const [isLoading, setIsLoading] = useState(false); @@ -189,7 +188,7 @@ export const ProfileForm = observer(function ProfileForm(props: TProfileFormProp )} />
-
+
-
-
- -
+
+
- - {({ open }) => ( - <> - - {t("deactivate_account")} - - - - -
- {t("deactivate_account_description")} -
- -
-
-
-
- - )} -
+
+ setDeactivateAccountModal(true)}> + {t("deactivate_account")} + + } + /> +
); }); diff --git a/apps/web/core/components/settings/profile/content/pages/general/index.ts b/apps/web/core/components/settings/profile/content/pages/general/index.ts new file mode 100644 index 0000000000..1efe34c51e --- /dev/null +++ b/apps/web/core/components/settings/profile/content/pages/general/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/page.tsx b/apps/web/core/components/settings/profile/content/pages/general/root.tsx similarity index 61% rename from apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/page.tsx rename to apps/web/core/components/settings/profile/content/pages/general/root.tsx index 7e52686778..47673520e7 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/page.tsx +++ b/apps/web/core/components/settings/profile/content/pages/general/root.tsx @@ -2,22 +2,22 @@ import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; // components import { PageHead } from "@/components/core/page-title"; -import { ProfileForm } from "@/components/profile/form"; // hooks import { useUser } from "@/hooks/store/user"; +// local imports +import { GeneralProfileSettingsForm } from "./form"; -function ProfileSettingsPage() { +export const GeneralProfileSettings = observer(function GeneralProfileSettings() { const { t } = useTranslation(); // store hooks const { data: currentUser, userProfile } = useUser(); - if (!currentUser) return <>; + if (!currentUser) return null; + return ( <> - + ); -} - -export default observer(ProfileSettingsPage); +}); diff --git a/apps/web/core/components/settings/profile/content/pages/index.ts b/apps/web/core/components/settings/profile/content/pages/index.ts new file mode 100644 index 0000000000..030086c2e0 --- /dev/null +++ b/apps/web/core/components/settings/profile/content/pages/index.ts @@ -0,0 +1,12 @@ +import { lazy } from "react"; +// plane imports +import type { TProfileSettingsTabs } from "@plane/types"; + +export const PROFILE_SETTINGS_PAGES_MAP: Record> = { + general: lazy(() => import("./general").then((m) => ({ default: m.GeneralProfileSettings }))), + preferences: lazy(() => import("./preferences").then((m) => ({ default: m.PreferencesProfileSettings }))), + notifications: lazy(() => import("./notifications").then((m) => ({ default: m.NotificationsProfileSettings }))), + security: lazy(() => import("./security").then((m) => ({ default: m.SecurityProfileSettings }))), + activity: lazy(() => import("./activity").then((m) => ({ default: m.ActivityProfileSettings }))), + "api-tokens": lazy(() => import("./api-tokens").then((m) => ({ default: m.APITokensProfileSettings }))), +}; diff --git a/apps/web/core/components/settings/profile/content/pages/notifications/email-notification-form.tsx b/apps/web/core/components/settings/profile/content/pages/notifications/email-notification-form.tsx new file mode 100644 index 0000000000..d4e46a6330 --- /dev/null +++ b/apps/web/core/components/settings/profile/content/pages/notifications/email-notification-form.tsx @@ -0,0 +1,161 @@ +import { useEffect } from "react"; +import { observer } from "mobx-react"; +import { Controller, useForm } from "react-hook-form"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { TOAST_TYPE, setToast } from "@plane/propel/toast"; +import type { IUserEmailNotificationSettings } from "@plane/types"; +import { ToggleSwitch } from "@plane/ui"; +// components +import { SettingsControlItem } from "@/components/settings/control-item"; +// services +import { UserService } from "@/services/user.service"; + +type Props = { + data: IUserEmailNotificationSettings; +}; + +// services +const userService = new UserService(); + +export const NotificationsProfileSettingsForm = observer(function NotificationsProfileSettingsForm(props: Props) { + const { data } = props; + // translation + const { t } = useTranslation(); + // form data + const { control, reset } = useForm({ + defaultValues: { + ...data, + }, + }); + + const handleSettingChange = async (key: keyof IUserEmailNotificationSettings, value: boolean) => { + try { + await userService.updateCurrentUserEmailNotificationSettings({ + [key]: value, + }); + setToast({ + title: t("success"), + type: TOAST_TYPE.SUCCESS, + message: t("email_notification_setting_updated_successfully"), + }); + } catch (_error) { + setToast({ + title: t("error"), + type: TOAST_TYPE.ERROR, + message: t("failed_to_update_email_notification_setting"), + }); + } + }; + + useEffect(() => { + reset(data); + }, [reset, data]); + + return ( +
+ ( + { + onChange(newValue); + handleSettingChange("property_change", newValue); + }} + size="sm" + /> + )} + /> + } + /> + ( + { + onChange(newValue); + handleSettingChange("state_change", newValue); + }} + size="sm" + /> + )} + /> + } + /> +
+ ( + { + onChange(newValue); + handleSettingChange("issue_completed", newValue); + }} + size="sm" + /> + )} + /> + } + /> +
+ ( + { + onChange(newValue); + handleSettingChange("comment", newValue); + }} + size="sm" + /> + )} + /> + } + /> + ( + { + onChange(newValue); + handleSettingChange("mention", newValue); + }} + size="sm" + /> + )} + /> + } + /> +
+ ); +}); diff --git a/apps/web/core/components/settings/profile/content/pages/notifications/index.ts b/apps/web/core/components/settings/profile/content/pages/notifications/index.ts new file mode 100644 index 0000000000..1efe34c51e --- /dev/null +++ b/apps/web/core/components/settings/profile/content/pages/notifications/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/notifications/page.tsx b/apps/web/core/components/settings/profile/content/pages/notifications/root.tsx similarity index 60% rename from apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/notifications/page.tsx rename to apps/web/core/components/settings/profile/content/pages/notifications/root.tsx index c4d05e1f88..7aabc3cb9e 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/account/notifications/page.tsx +++ b/apps/web/core/components/settings/profile/content/pages/notifications/root.tsx @@ -1,17 +1,18 @@ import useSWR from "swr"; +import { observer } from "mobx-react"; // plane imports import { useTranslation } from "@plane/i18n"; // components -import { PageHead } from "@/components/core/page-title"; -import { EmailNotificationForm } from "@/components/profile/notification/email-notification-form"; -import { SettingsHeading } from "@/components/settings/heading"; +import { ProfileSettingsHeading } from "@/components/settings/profile/heading"; import { EmailSettingsLoader } from "@/components/ui/loader/settings/email"; // services import { UserService } from "@/services/user.service"; +// local imports +import { NotificationsProfileSettingsForm } from "./email-notification-form"; const userService = new UserService(); -export default function ProfileNotificationPage() { +export const NotificationsProfileSettings = observer(function NotificationsProfileSettings() { const { t } = useTranslation(); // fetching user email notification settings const { data, isLoading } = useSWR("CURRENT_USER_EMAIL_NOTIFICATION_SETTINGS", () => @@ -23,14 +24,14 @@ export default function ProfileNotificationPage() { } return ( - <> - - - + - - +
+ +
+
); -} +}); diff --git a/apps/web/core/components/settings/profile/content/pages/preferences/default-list.tsx b/apps/web/core/components/settings/profile/content/pages/preferences/default-list.tsx new file mode 100644 index 0000000000..0952adca93 --- /dev/null +++ b/apps/web/core/components/settings/profile/content/pages/preferences/default-list.tsx @@ -0,0 +1,17 @@ +import { observer } from "mobx-react"; +// components +import { ThemeSwitcher } from "ce/components/preferences/theme-switcher"; + +export const ProfileSettingsDefaultPreferencesList = observer(function ProfileSettingsDefaultPreferencesList() { + return ( +
+ +
+ ); +}); diff --git a/apps/web/core/components/settings/profile/content/pages/preferences/index.ts b/apps/web/core/components/settings/profile/content/pages/preferences/index.ts new file mode 100644 index 0000000000..1efe34c51e --- /dev/null +++ b/apps/web/core/components/settings/profile/content/pages/preferences/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/apps/web/core/components/settings/profile/content/pages/preferences/language-and-timezone-list.tsx b/apps/web/core/components/settings/profile/content/pages/preferences/language-and-timezone-list.tsx new file mode 100644 index 0000000000..1afcff638d --- /dev/null +++ b/apps/web/core/components/settings/profile/content/pages/preferences/language-and-timezone-list.tsx @@ -0,0 +1,102 @@ +import { observer } from "mobx-react"; +// plane imports +import { SUPPORTED_LANGUAGES, useTranslation } from "@plane/i18n"; +import { TOAST_TYPE, setToast } from "@plane/propel/toast"; +import { CustomSelect } from "@plane/ui"; +// components +import { TimezoneSelect } from "@/components/global"; +import { StartOfWeekPreference } from "@/components/profile/start-of-week-preference"; +import { SettingsControlItem } from "@/components/settings/control-item"; +// hooks +import { useUser, useUserProfile } from "@/hooks/store/user"; + +export const ProfileSettingsLanguageAndTimezonePreferencesList = observer( + function ProfileSettingsLanguageAndTimezonePreferencesList() { + // store hooks + const { + data: user, + updateCurrentUser, + userProfile: { data: profile }, + } = useUser(); + const { updateUserProfile } = useUserProfile(); + // translation + const { t } = useTranslation(); + + const handleTimezoneChange = async (value: string) => { + try { + await updateCurrentUser({ user_timezone: value }); + setToast({ + title: "Success!", + message: "Timezone updated successfully", + type: TOAST_TYPE.SUCCESS, + }); + } catch (_error) { + setToast({ + title: "Error!", + message: "Failed to update timezone", + type: TOAST_TYPE.ERROR, + }); + } + }; + + const handleLanguageChange = async (value: string) => { + try { + await updateUserProfile({ language: value }); + setToast({ + title: "Success!", + message: "Language updated successfully", + type: TOAST_TYPE.SUCCESS, + }); + } catch (_error) { + setToast({ + title: "Error!", + message: "Failed to update language", + type: TOAST_TYPE.ERROR, + }); + } + }; + + const getLanguageLabel = (value: string) => { + const selectedLanguage = SUPPORTED_LANGUAGES.find((l) => l.value === value); + if (!selectedLanguage) return value; + return selectedLanguage.label; + }; + + return ( +
+ } + /> + + {SUPPORTED_LANGUAGES.map((item) => ( + + {item.label} + + ))} + + } + /> + +
+ ); + } +); diff --git a/apps/web/core/components/settings/profile/content/pages/preferences/root.tsx b/apps/web/core/components/settings/profile/content/pages/preferences/root.tsx new file mode 100644 index 0000000000..38e53fcabe --- /dev/null +++ b/apps/web/core/components/settings/profile/content/pages/preferences/root.tsx @@ -0,0 +1,36 @@ +import { observer } from "mobx-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// components +import { ProfileSettingsHeading } from "@/components/settings/profile/heading"; +// hooks +import { useUserProfile } from "@/hooks/store/user"; +// local imports +import { ProfileSettingsDefaultPreferencesList } from "./default-list"; +import { ProfileSettingsLanguageAndTimezonePreferencesList } from "./language-and-timezone-list"; + +export const PreferencesProfileSettings = observer(function PreferencesProfileSettings() { + const { t } = useTranslation(); + // hooks + const { data: userProfile } = useUserProfile(); + + if (!userProfile) return null; + + return ( +
+ +
+
+ +
+
+
{t("language_and_time")}
+ +
+
+
+ ); +}); diff --git a/apps/web/app/(all)/profile/security/page.tsx b/apps/web/core/components/settings/profile/content/pages/security.tsx similarity index 73% rename from apps/web/app/(all)/profile/security/page.tsx rename to apps/web/core/components/settings/profile/content/pages/security.tsx index aad3001fa6..fa6916b12b 100644 --- a/apps/web/app/(all)/profile/security/page.tsx +++ b/apps/web/core/components/settings/profile/content/pages/security.tsx @@ -8,11 +8,9 @@ import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { Input, PasswordStrengthIndicator } from "@plane/ui"; -// components import { getPasswordStrength } from "@plane/utils"; -import { PageHead } from "@/components/core/page-title"; -import { ProfileSettingContentHeader } from "@/components/profile/profile-setting-content-header"; -import { ProfileSettingContentWrapper } from "@/components/profile/profile-setting-content-wrapper"; +// components +import { ProfileSettingsHeading } from "@/components/settings/profile/heading"; // helpers import { authErrorHandler } from "@/helpers/authentication.helper"; import type { EAuthenticationErrorCodes } from "@/helpers/authentication.helper"; @@ -41,7 +39,7 @@ const defaultShowPassword = { confirmPassword: false, }; -function SecurityPage() { +export const SecurityProfileSettings = observer(function SecurityProfileSettings() { // store const { data: currentUser, changePassword } = useUser(); // states @@ -89,9 +87,12 @@ function SecurityPage() { message: t("auth.common.password.toast.change_password.success.message"), }); } catch (error: unknown) { - const err = error as Error & { error_code?: string }; - const code = err.error_code?.toString(); - const errorInfo = code ? authErrorHandler(code as EAuthenticationErrorCodes) : undefined; + let errorInfo = undefined; + if (error instanceof Error) { + const code = "error_code" in error ? error.error_code?.toString() : undefined; + errorInfo = code ? authErrorHandler(code as EAuthenticationErrorCodes) : undefined; + } + setToast({ type: TOAST_TYPE.ERROR, title: errorInfo?.title ?? t("auth.common.password.toast.error.title"), @@ -117,52 +118,51 @@ function SecurityPage() { const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length; return ( - <> - - - -
-
- {oldPasswordRequired && ( -
-

{t("auth.common.password.current_password.label")}

-
- ( - - )} - /> - {showPassword?.oldPassword ? ( - handleShowPassword("oldPassword")} - /> - ) : ( - handleShowPassword("oldPassword")} +
+ + +
+ {oldPasswordRequired && ( +
+

{t("auth.common.password.current_password.label")}

+
+ ( + )} -
- {errors.old_password && ( - {errors.old_password.message} + /> + {showPassword?.oldPassword ? ( + handleShowPassword("oldPassword")} + /> + ) : ( + handleShowPassword("oldPassword")} + /> )}
- )} -
+ {errors.old_password && ( + {errors.old_password.message} + )} +
+ )} +
+

{t("auth.common.password.new_password.label")}

)}
-
+

{t("auth.common.password.confirm_password.label")}

- -
-
- - - +
+ +
); -} - -export default observer(SecurityPage); +}); diff --git a/apps/web/core/components/settings/profile/content/root.tsx b/apps/web/core/components/settings/profile/content/root.tsx new file mode 100644 index 0000000000..0b4e721c2b --- /dev/null +++ b/apps/web/core/components/settings/profile/content/root.tsx @@ -0,0 +1,32 @@ +import { Suspense } from "react"; +import { observer } from "mobx-react"; +// plane imports +import { ScrollArea } from "@plane/propel/scrollarea"; +import type { TProfileSettingsTabs } from "@plane/types"; +import { cn } from "@plane/utils"; +// local imports +import { PROFILE_SETTINGS_PAGES_MAP } from "./pages"; + +type Props = { + activeTab: TProfileSettingsTabs; + className?: string; +}; + +export const ProfileSettingsContent = observer(function ProfileSettingsContent(props: Props) { + const { activeTab, className } = props; + const PageComponent = PROFILE_SETTINGS_PAGES_MAP[activeTab]; + + return ( + + + + + + ); +}); diff --git a/apps/web/core/components/settings/profile/heading.tsx b/apps/web/core/components/settings/profile/heading.tsx new file mode 100644 index 0000000000..2a1f6433a0 --- /dev/null +++ b/apps/web/core/components/settings/profile/heading.tsx @@ -0,0 +1,21 @@ +// plane imports +import { cn } from "@plane/ui"; + +type Props = { + className?: string; + control?: React.ReactNode; + description?: React.ReactNode; + title?: React.ReactNode; +}; + +export function ProfileSettingsHeading({ className, control, description, title }: Props) { + return ( +
+
+ {title &&
{title}
} + {description &&

{description}

} +
+ {control} +
+ ); +} diff --git a/apps/web/core/components/settings/profile/modal.tsx b/apps/web/core/components/settings/profile/modal.tsx new file mode 100644 index 0000000000..d1ffbd33d9 --- /dev/null +++ b/apps/web/core/components/settings/profile/modal.tsx @@ -0,0 +1,53 @@ +import { useCallback } from "react"; +import { X } from "lucide-react"; +import { observer } from "mobx-react"; +// plane imports +import { IconButton } from "@plane/propel/icon-button"; +import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; +// hooks +import { useCommandPalette } from "@/hooks/store/use-command-palette"; +// local imports +import { ProfileSettingsContent } from "./content"; +import { ProfileSettingsSidebarRoot } from "./sidebar"; + +export const ProfileSettingsModal = observer(function ProfileSettingsModal() { + // store hooks + const { profileSettingsModal, toggleProfileSettingsModal } = useCommandPalette(); + // derived values + const activeTab = profileSettingsModal.activeTab ?? "general"; + + const handleClose = useCallback(() => { + toggleProfileSettingsModal({ + isOpen: false, + }); + setTimeout(() => { + toggleProfileSettingsModal({ + activeTab: null, + }); + }, 300); + }, [toggleProfileSettingsModal]); + + return ( + +
+
+ toggleProfileSettingsModal({ activeTab: tab })} + /> + +
+
+ +
+
+
+ ); +}); diff --git a/apps/web/core/components/settings/profile/sidebar/header.tsx b/apps/web/core/components/settings/profile/sidebar/header.tsx new file mode 100644 index 0000000000..1bd4f1b1c7 --- /dev/null +++ b/apps/web/core/components/settings/profile/sidebar/header.tsx @@ -0,0 +1,31 @@ +import { observer } from "mobx-react"; +// plane imports +import { Avatar } from "@plane/ui"; +// hooks +import { useUser } from "@/hooks/store/user"; +import { getFileURL } from "@plane/utils"; + +export const ProfileSettingsSidebarHeader = observer(function ProfileSettingsSidebarHeader() { + // store hooks + const { data: currentUser } = useUser(); + + return ( +
+
+ +
+
+

+ {currentUser?.first_name} {currentUser?.last_name} +

+

{currentUser?.email}

+
+
+ ); +}); diff --git a/apps/web/core/components/settings/profile/sidebar/index.ts b/apps/web/core/components/settings/profile/sidebar/index.ts new file mode 100644 index 0000000000..1efe34c51e --- /dev/null +++ b/apps/web/core/components/settings/profile/sidebar/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/apps/web/core/components/settings/profile/sidebar/item-categories.tsx b/apps/web/core/components/settings/profile/sidebar/item-categories.tsx new file mode 100644 index 0000000000..b204e4110d --- /dev/null +++ b/apps/web/core/components/settings/profile/sidebar/item-categories.tsx @@ -0,0 +1,66 @@ +import type React from "react"; +import type { LucideIcon } from "lucide-react"; +import { Activity, Bell, CircleUser, KeyRound, LockIcon, Settings2 } from "lucide-react"; +import { observer } from "mobx-react"; +import { useParams } from "react-router"; +// plane imports +import { GROUPED_PROFILE_SETTINGS, PROFILE_SETTINGS_CATEGORIES } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import type { ISvgIcons } from "@plane/propel/icons"; +import type { TProfileSettingsTabs } from "@plane/types"; +// local imports +import { SettingsSidebarItem } from "../../sidebar/item"; +import { ProfileSettingsSidebarWorkspaceOptions } from "./workspace-options"; + +const ICONS: Record> = { + general: CircleUser, + security: LockIcon, + activity: Activity, + preferences: Settings2, + notifications: Bell, + "api-tokens": KeyRound, +}; + +type Props = { + activeTab: TProfileSettingsTabs; + updateActiveTab: (tab: TProfileSettingsTabs) => void; +}; + +export const ProfileSettingsSidebarItemCategories = observer(function ProfileSettingsSidebarItemCategories( + props: Props +) { + const { activeTab, updateActiveTab } = props; + // params + const { profileTabId } = useParams(); + // translation + const { t } = useTranslation(); + + return ( +
+ {PROFILE_SETTINGS_CATEGORIES.map((category) => { + const categoryItems = GROUPED_PROFILE_SETTINGS[category]; + + if (categoryItems.length === 0) return null; + + return ( +
+
{t(category)}
+
+ {categoryItems.map((item) => ( + updateActiveTab(item.key)} + isActive={activeTab === item.key} + icon={ICONS[item.key]} + label={t(item.i18n_label)} + /> + ))} +
+
+ ); + })} + {profileTabId && } +
+ ); +}); diff --git a/apps/web/core/components/settings/profile/sidebar/root.tsx b/apps/web/core/components/settings/profile/sidebar/root.tsx new file mode 100644 index 0000000000..703218e04f --- /dev/null +++ b/apps/web/core/components/settings/profile/sidebar/root.tsx @@ -0,0 +1,29 @@ +// plane imports +import { ScrollArea } from "@plane/propel/scrollarea"; +import type { TProfileSettingsTabs } from "@plane/types"; +import { cn } from "@plane/utils"; +// local imports +import { ProfileSettingsSidebarHeader } from "./header"; +import { ProfileSettingsSidebarItemCategories } from "./item-categories"; + +type Props = { + activeTab: TProfileSettingsTabs; + className?: string; + updateActiveTab: (tab: TProfileSettingsTabs) => void; +}; + +export function ProfileSettingsSidebarRoot(props: Props) { + const { activeTab, className, updateActiveTab } = props; + + return ( + + + + + ); +} diff --git a/apps/web/core/components/settings/profile/sidebar/workspace-options.tsx b/apps/web/core/components/settings/profile/sidebar/workspace-options.tsx new file mode 100644 index 0000000000..a695437ad3 --- /dev/null +++ b/apps/web/core/components/settings/profile/sidebar/workspace-options.tsx @@ -0,0 +1,50 @@ +import { CirclePlus, Mails } from "lucide-react"; +import { observer } from "mobx-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// components +import { SettingsSidebarItem } from "@/components/settings/sidebar/item"; +import { WorkspaceLogo } from "@/components/workspace/logo"; +// hooks +import { useWorkspace } from "@/hooks/store/use-workspace"; + +export const ProfileSettingsSidebarWorkspaceOptions = observer(function ProfileSettingsSidebarWorkspaceOptions() { + // store hooks + const { workspaces } = useWorkspace(); + // translation + const { t } = useTranslation(); + + return ( +
+
{t("workspace")}
+
+ {Object.values(workspaces).map((workspace) => ( + } + label={workspace.name} + isActive={false} + /> + ))} +
+ + +
+
+
+ ); +}); diff --git a/apps/web/core/components/settings/project/content/feature-control-item.tsx b/apps/web/core/components/settings/project/content/feature-control-item.tsx new file mode 100644 index 0000000000..227e8cd52b --- /dev/null +++ b/apps/web/core/components/settings/project/content/feature-control-item.tsx @@ -0,0 +1,60 @@ +import { observer } from "mobx-react"; +// plane imports +import { setPromiseToast } from "@plane/propel/toast"; +import type { IProject } from "@plane/types"; +import { ToggleSwitch } from "@plane/ui"; +// components +import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item"; +// hooks +import { useProject } from "@/hooks/store/use-project"; + +type Props = { + description?: React.ReactNode; + disabled?: boolean; + projectId: string; + featureProperty: keyof IProject; + title: React.ReactNode; + value: boolean; + workspaceSlug: string; +}; + +export const ProjectSettingsFeatureControlItem = observer(function ProjectSettingsFeatureControlItem(props: Props) { + const { description, disabled, featureProperty, projectId, title, value, workspaceSlug } = props; + // store hooks + const { getProjectById, updateProject } = useProject(); + // derived values + const currentProjectDetails = getProjectById(projectId); + + const handleSubmit = () => { + if (!workspaceSlug || !projectId || !currentProjectDetails) return; + + // making the request to update the project feature + const settingsPayload = { + [featureProperty]: !currentProjectDetails?.[featureProperty], + }; + const updateProjectPromise = updateProject(workspaceSlug, projectId, settingsPayload); + + setPromiseToast(updateProjectPromise, { + loading: "Updating project feature...", + success: { + title: "Success!", + message: () => "Project feature updated successfully.", + }, + error: { + title: "Error!", + message: () => "Something went wrong while updating project feature. Please try again.", + }, + }); + void updateProjectPromise.then(() => { + return undefined; + }); + }; + + return ( + } + /> + ); +}); diff --git a/apps/web/core/components/settings/project/sidebar/header.tsx b/apps/web/core/components/settings/project/sidebar/header.tsx new file mode 100644 index 0000000000..aa255908f5 --- /dev/null +++ b/apps/web/core/components/settings/project/sidebar/header.tsx @@ -0,0 +1,58 @@ +import { ArrowLeft } from "lucide-react"; +import { observer } from "mobx-react"; +// plane imports +import { ROLE_DETAILS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Logo } from "@plane/propel/emoji-icon-picker"; +import { IconButton } from "@plane/propel/icon-button"; +// hooks +import { useUserPermissions } from "@/hooks/store/user"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useProject } from "@/hooks/store/use-project"; +import { useWorkspace } from "@/hooks/store/use-workspace"; + +type Props = { + projectId: string; +}; + +export const ProjectSettingsSidebarHeader = observer(function ProjectSettingsSidebarHeader(props: Props) { + const { projectId } = props; + // router + const router = useAppRouter(); + // store hooks + const { getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions(); + const { currentWorkspace } = useWorkspace(); + const { getPartialProjectById } = useProject(); + // derived values + const projectDetails = getPartialProjectById(projectId); + const currentProjectRole = currentWorkspace?.slug + ? getProjectRoleByWorkspaceSlugAndProjectId(currentWorkspace.slug, projectId) + : undefined; + // translation + const { t } = useTranslation(); + + if (!currentProjectRole) return null; + + return ( +
+
+ router.push(`/${currentWorkspace?.slug}/projects/${projectId}/issues/`)} + /> +

Project settings

+
+
+
+ +
+
+

{projectDetails?.name}

+

{t(ROLE_DETAILS[currentProjectRole].i18n_title)}

+
+
+
+ ); +}); diff --git a/apps/web/core/components/settings/project/sidebar/item-categories.tsx b/apps/web/core/components/settings/project/sidebar/item-categories.tsx new file mode 100644 index 0000000000..d368a27928 --- /dev/null +++ b/apps/web/core/components/settings/project/sidebar/item-categories.tsx @@ -0,0 +1,67 @@ +import { observer } from "mobx-react"; +import { usePathname } from "next/navigation"; +import { useParams } from "react-router"; +// plane imports +import { EUserPermissionsLevel, GROUPED_PROJECT_SETTINGS, PROJECT_SETTINGS_CATEGORIES } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +// components +import { SettingsSidebarItem } from "@/components/settings/sidebar/item"; +// hooks +import { useUserPermissions } from "@/hooks/store/user"; +// local imports +import { PROJECT_SETTINGS_ICONS } from "./item-icon"; + +type Props = { + projectId: string; +}; + +export const ProjectSettingsSidebarItemCategories = observer(function ProjectSettingsSidebarItemCategories( + props: Props +) { + const { projectId } = props; + // params + const { workspaceSlug } = useParams(); + const pathname = usePathname(); + // store hooks + const { allowPermissions } = useUserPermissions(); + // translation + const { t } = useTranslation(); + + return ( +
+ {PROJECT_SETTINGS_CATEGORIES.map((category) => { + const categoryItems = GROUPED_PROJECT_SETTINGS[category]; + const accessibleItems = categoryItems.filter((item) => + allowPermissions(item.access, EUserPermissionsLevel.PROJECT, workspaceSlug, projectId) + ); + + if (accessibleItems.length === 0) return null; + + return ( +
+
{t(category)}
+
+ {accessibleItems.map((item) => { + const isItemActive = + item.href === "" + ? pathname === `/${workspaceSlug}/settings/projects/${projectId}${item.href}/` + : new RegExp(`^/${workspaceSlug}/settings/projects/${projectId}${item.href}/`).test(pathname); + + return ( + + ); + })} +
+
+ ); + })} +
+ ); +}); diff --git a/apps/web/core/components/settings/project/sidebar/item-icon.tsx b/apps/web/core/components/settings/project/sidebar/item-icon.tsx new file mode 100644 index 0000000000..6988776b3c --- /dev/null +++ b/apps/web/core/components/settings/project/sidebar/item-icon.tsx @@ -0,0 +1,31 @@ +import type { LucideIcon } from "lucide-react"; +import { Users, Zap } from "lucide-react"; +// plane imports +import type { ISvgIcons } from "@plane/propel/icons"; +import { + CycleIcon, + EstimatePropertyIcon, + IntakeIcon, + LabelPropertyIcon, + ModuleIcon, + PageIcon, + StatePropertyIcon, + ViewsIcon, +} from "@plane/propel/icons"; +import type { TProjectSettingsTabs } from "@plane/types"; +// components +import { SettingIcon } from "@/components/icons/attachment"; + +export const PROJECT_SETTINGS_ICONS: Record> = { + general: SettingIcon, + members: Users, + features_cycles: CycleIcon, + features_modules: ModuleIcon, + features_views: ViewsIcon, + features_pages: PageIcon, + features_intake: IntakeIcon, + states: StatePropertyIcon, + labels: LabelPropertyIcon, + estimates: EstimatePropertyIcon, + automations: Zap, +}; diff --git a/apps/web/core/components/settings/project/sidebar/nav-item-children.tsx b/apps/web/core/components/settings/project/sidebar/nav-item-children.tsx deleted file mode 100644 index 5c79d9241d..0000000000 --- a/apps/web/core/components/settings/project/sidebar/nav-item-children.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { range } from "lodash-es"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { usePathname, useParams } from "next/navigation"; -import { EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Loader } from "@plane/ui"; -import { cn } from "@plane/utils"; -import { useProject } from "@/hooks/store/use-project"; -import { useUserPermissions, useUserSettings } from "@/hooks/store/user"; -import { PROJECT_SETTINGS_LINKS } from "@/plane-web/constants/project"; -import { getProjectSettingsPageLabelI18nKey } from "@/plane-web/helpers/project-settings"; - -export const NavItemChildren = observer(function NavItemChildren(props: { projectId: string }) { - const { projectId } = props; - const { workspaceSlug } = useParams(); - const pathname = usePathname(); - // mobx store - const { getProjectById } = useProject(); - const { allowPermissions } = useUserPermissions(); - const { t } = useTranslation(); - const { toggleSidebar } = useUserSettings(); - - // derived values - const currentProject = getProjectById(projectId); - - if (!currentProject) { - return ( -
-
- - {range(8).map((index) => ( - - ))} - -
-
- ); - } - - return ( -
-
-
- {PROJECT_SETTINGS_LINKS.map((link) => { - const isActive = link.highlight(pathname, `/${workspaceSlug}/settings/projects/${projectId}`); - return ( - allowPermissions( - link.access, - EUserPermissionsLevel.PROJECT, - workspaceSlug?.toString() ?? "", - projectId?.toString() ?? "" - ) && ( - toggleSidebar(true)} - > -
- {t(getProjectSettingsPageLabelI18nKey(link.key, link.i18n_label))} -
- - ) - ); - })} -
-
-
- ); -}); diff --git a/apps/web/core/components/settings/project/sidebar/root.tsx b/apps/web/core/components/settings/project/sidebar/root.tsx index 4a51f55baa..2764e96286 100644 --- a/apps/web/core/components/settings/project/sidebar/root.tsx +++ b/apps/web/core/components/settings/project/sidebar/root.tsx @@ -1,52 +1,26 @@ -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; // plane imports -import { PROJECT_SETTINGS_CATEGORIES, PROJECT_SETTINGS_CATEGORY } from "@plane/constants"; -import { Logo } from "@plane/propel/emoji-icon-picker"; -import { getUserRole } from "@plane/utils"; -// components -// hooks -import { useProject } from "@/hooks/store/use-project"; +import { ScrollArea } from "@plane/propel/scrollarea"; // local imports -import { SettingsSidebar } from "../../sidebar"; -import { NavItemChildren } from "./nav-item-children"; +import { ProjectSettingsSidebarHeader } from "./header"; +import { ProjectSettingsSidebarItemCategories } from "./item-categories"; -type TProjectSettingsSidebarProps = { - isMobile?: boolean; +type Props = { + projectId: string; }; -export const ProjectSettingsSidebar = observer(function ProjectSettingsSidebar(props: TProjectSettingsSidebarProps) { - const { isMobile = false } = props; - const { workspaceSlug } = useParams(); - // store hooks - const { joinedProjectIds, projectMap } = useProject(); - - const groupedProject = joinedProjectIds.map((projectId) => ({ - key: projectId, - i18n_label: projectMap[projectId].name, - href: `/settings/projects/${projectId}`, - icon: , - })); +export function ProjectSettingsSidebarRoot(props: Props) { + const { projectId } = props; return ( - { - const role = projectMap[key].member_role; - return ( -
- {role ? getUserRole(role)?.toLowerCase() : "Guest"} -
- ); - }} - shouldRender - renderChildren={(key: string) => } - /> + + + + ); -}); +} diff --git a/apps/web/core/components/settings/sidebar/header.tsx b/apps/web/core/components/settings/sidebar/header.tsx deleted file mode 100644 index ca5881f6fa..0000000000 --- a/apps/web/core/components/settings/sidebar/header.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { observer } from "mobx-react"; -// plane imports -import { getUserRole } from "@plane/utils"; -// components -import { WorkspaceLogo } from "@/components/workspace/logo"; -// hooks -import { useWorkspace } from "@/hooks/store/use-workspace"; -// plane web imports -import { SubscriptionPill } from "@/plane-web/components/common/subscription/subscription-pill"; - -export const SettingsSidebarHeader = observer(function SettingsSidebarHeader(props: { - customHeader?: React.ReactNode; -}) { - const { customHeader } = props; - const { currentWorkspace } = useWorkspace(); - return customHeader - ? customHeader - : currentWorkspace && ( -
-
- -
-
- {currentWorkspace.name ?? "Workspace"} -
-
- {getUserRole(currentWorkspace.role)?.toLowerCase() || "guest"} -
-
-
-
- -
-
- ); -}); diff --git a/apps/web/core/components/settings/sidebar/item.tsx b/apps/web/core/components/settings/sidebar/item.tsx new file mode 100644 index 0000000000..2fdd804454 --- /dev/null +++ b/apps/web/core/components/settings/sidebar/item.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import Link from "next/link"; +// plane imports +import { cn } from "@plane/utils"; +import type { LucideIcon } from "lucide-react"; +import type { ISvgIcons } from "@plane/propel/icons"; + +type Props = { + isActive: boolean; + label: string; +} & ({ as: "button"; onClick: () => void } | { as: "link"; href: string }) & + ( + | { + icon: LucideIcon | React.FC; + } + | { iconNode: React.ReactElement } + ); + +export function SettingsSidebarItem(props: Props) { + const { as, isActive, label } = props; + // common class + const className = cn( + "flex items-center gap-2 py-1.5 px-2 rounded-lg text-body-sm-medium text-secondary text-left transition-colors", + { + "bg-layer-transparent-selected text-primary": isActive, + "hover:bg-layer-transparent-hover": !isActive, + } + ); + // common content + const content = ( + <> + {"icon" in props ? ( + {} + ) : ( + props.iconNode + )} + {label} + + ); + + if (as === "button") { + return ( + + ); + } + + return ( + + {content} + + ); +} diff --git a/apps/web/core/components/settings/sidebar/nav-item.tsx b/apps/web/core/components/settings/sidebar/nav-item.tsx deleted file mode 100644 index 0bdf797dcb..0000000000 --- a/apps/web/core/components/settings/sidebar/nav-item.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { useParams } from "next/navigation"; -import { Disclosure } from "@headlessui/react"; -// plane imports -import { useTranslation } from "@plane/i18n"; -import type { EUserWorkspaceRoles } from "@plane/types"; -import { cn, joinUrlPath } from "@plane/utils"; -// hooks -import { useUserSettings } from "@/hooks/store/user"; - -export type TSettingItem = { - key: string; - i18n_label: string; - href: string; - access?: EUserWorkspaceRoles[]; - icon?: React.ReactNode; -}; -export type TSettingsSidebarNavItemProps = { - workspaceSlug: string; - setting: TSettingItem; - isActive: boolean | ((data: { href: string }) => boolean); - actionIcons?: (props: { type: string; size?: number; className?: string }) => React.ReactNode; - appendItemsToTitle?: (key: string) => React.ReactNode; - renderChildren?: (key: string) => React.ReactNode; -}; - -const SettingsSidebarNavItem = observer(function SettingsSidebarNavItem(props: TSettingsSidebarNavItemProps) { - const { workspaceSlug, setting, isActive, actionIcons, appendItemsToTitle, renderChildren } = props; - // router - const { projectId } = useParams(); - // i18n - const { t } = useTranslation(); - // state - const [isExpanded, setIsExpanded] = useState(projectId === setting.key); - // hooks - const { toggleSidebar } = useUserSettings(); - // derived - const isItemActive = typeof isActive === "function" ? isActive(setting) : isActive; - const buttonClass = cn("flex w-full items-center px-2 py-1.5 rounded-sm text-secondary justify-between", { - "bg-layer-transparent-active hover:bg-layer-transparent-active": isItemActive, - "hover:bg-layer-transparent-hover": !isItemActive, - }); - - const titleElement = ( - <> -
- {setting.icon - ? setting.icon - : actionIcons && actionIcons({ type: setting.key, size: 16, className: "w-4 h-4" })} -
{t(setting.i18n_label)}
-
- {appendItemsToTitle?.(setting.key)} - - ); - - return ( - - setIsExpanded(!isExpanded)} - > - {renderChildren ? ( -
{titleElement}
- ) : ( - toggleSidebar(true)} - > - {titleElement} - - )} -
- {/* Nested Navigation */} - {isExpanded && ( - -
- {renderChildren?.(setting.key)} - - )} - - ); -}); - -export default SettingsSidebarNavItem; diff --git a/apps/web/core/components/settings/sidebar/root.tsx b/apps/web/core/components/settings/sidebar/root.tsx deleted file mode 100644 index 74433a69cb..0000000000 --- a/apps/web/core/components/settings/sidebar/root.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { observer } from "mobx-react"; -import { useTranslation } from "@plane/i18n"; -import { cn } from "@plane/utils"; -import { SettingsSidebarHeader } from "./header"; -import type { TSettingItem } from "./nav-item"; -import SettingsSidebarNavItem from "./nav-item"; - -interface SettingsSidebarProps { - isMobile?: boolean; - customHeader?: React.ReactNode; - categories: string[]; - groupedSettings: { - [key: string]: TSettingItem[]; - }; - workspaceSlug: string; - isActive: boolean | ((data: { href: string }) => boolean); - shouldRender: boolean | ((setting: TSettingItem) => boolean); - actionIcons?: (props: { type: string; size?: number; className?: string }) => React.ReactNode; - appendItemsToTitle?: (key: string) => React.ReactNode; - renderChildren?: (key: string) => React.ReactNode; -} - -export const SettingsSidebar = observer(function SettingsSidebar(props: SettingsSidebarProps) { - const { - isMobile = false, - customHeader, - categories, - groupedSettings, - workspaceSlug, - isActive, - shouldRender, - actionIcons, - appendItemsToTitle, - renderChildren, - } = props; - // hooks - const { t } = useTranslation(); - - return ( -
- {/* Header */} - - {/* Navigation */} -
- {categories.map((category) => { - if (groupedSettings[category].length === 0) return null; - return ( -
- {t(category)} -
- {groupedSettings[category].map( - (setting) => - (typeof shouldRender === "function" ? shouldRender(setting) : shouldRender) && ( - - ) - )} -
-
- ); - })} -
-
- ); -}); diff --git a/apps/web/core/components/settings/tabs.tsx b/apps/web/core/components/settings/tabs.tsx deleted file mode 100644 index eac70d34af..0000000000 --- a/apps/web/core/components/settings/tabs.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { observer } from "mobx-react"; -import Link from "next/link"; -import { useParams, usePathname } from "next/navigation"; -import { cn } from "@plane/utils"; -import { useProject } from "@/hooks/store/use-project"; - -const TABS = { - account: { - key: "account", - label: "Account", - href: `/settings/account/`, - }, - workspace: { - key: "workspace", - label: "Workspace", - href: `/settings/`, - }, - projects: { - key: "projects", - label: "Projects", - href: `/settings/projects/`, - }, -}; - -const SettingsTabs = observer(function SettingsTabs() { - // router - const pathname = usePathname(); - const { workspaceSlug } = useParams(); - // store hooks - const { joinedProjectIds } = useProject(); - - const currentTab = pathname.includes(TABS.projects.href) - ? TABS.projects - : pathname.includes(TABS.account.href) - ? TABS.account - : TABS.workspace; - - return ( -
- {Object.values(TABS).map((tab) => { - const isActive = currentTab?.key === tab.key; - const href = tab.key === TABS.projects.key ? `${tab.href}${joinedProjectIds[0] || ""}` : tab.href; - return ( - -
{tab.label}
- - ); - })} -
- ); -}); - -export default SettingsTabs; diff --git a/apps/web/core/components/settings/workspace/sidebar/header.tsx b/apps/web/core/components/settings/workspace/sidebar/header.tsx new file mode 100644 index 0000000000..534e50271f --- /dev/null +++ b/apps/web/core/components/settings/workspace/sidebar/header.tsx @@ -0,0 +1,60 @@ +import { ArrowLeft } from "lucide-react"; +import { observer } from "mobx-react"; +// plane imports +import { ROLE_DETAILS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { IconButton } from "@plane/propel/icon-button"; +// components +import { WorkspaceLogo } from "@/components/workspace/logo"; +// hooks +import { useUserPermissions } from "@/hooks/store/user"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useWorkspace } from "@/hooks/store/use-workspace"; +// plane web imports +import { SubscriptionPill } from "@/plane-web/components/common/subscription/subscription-pill"; + +export const WorkspaceSettingsSidebarHeader = observer(function WorkspaceSettingsSidebarHeader() { + // router + const router = useAppRouter(); + // store hooks + const { getWorkspaceRoleByWorkspaceSlug } = useUserPermissions(); + const { currentWorkspace } = useWorkspace(); + // derived values + const currentWorkspaceRole = currentWorkspace?.slug + ? getWorkspaceRoleByWorkspaceSlug(currentWorkspace.slug) + : undefined; + // translation + const { t } = useTranslation(); + + if (!currentWorkspaceRole) return null; + + return ( +
+
+ router.push(`/${currentWorkspace?.slug}/`)} + /> +

Workspace settings

+
+
+
+ +
+

{currentWorkspace?.name}

+

{t(ROLE_DETAILS[currentWorkspaceRole].i18n_title)}

+
+
+
+ +
+
+
+ ); +}); diff --git a/apps/web/core/components/settings/workspace/sidebar/index.ts b/apps/web/core/components/settings/workspace/sidebar/index.ts new file mode 100644 index 0000000000..1efe34c51e --- /dev/null +++ b/apps/web/core/components/settings/workspace/sidebar/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/apps/web/core/components/settings/workspace/sidebar/item-categories.tsx b/apps/web/core/components/settings/workspace/sidebar/item-categories.tsx new file mode 100644 index 0000000000..9223ba8c62 --- /dev/null +++ b/apps/web/core/components/settings/workspace/sidebar/item-categories.tsx @@ -0,0 +1,61 @@ +import { observer } from "mobx-react"; +import { usePathname } from "next/navigation"; +import { useParams } from "react-router"; +// plane imports +import { EUserPermissionsLevel, GROUPED_WORKSPACE_SETTINGS, WORKSPACE_SETTINGS_CATEGORIES } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { joinUrlPath } from "@plane/utils"; +// components +import { SettingsSidebarItem } from "@/components/settings/sidebar/item"; +// hooks +import { useUserPermissions } from "@/hooks/store/user"; +// local imports +import { WORKSPACE_SETTINGS_ICONS } from "./item-icon"; + +export const WorkspaceSettingsSidebarItemCategories = observer(function WorkspaceSettingsSidebarItemCategories() { + // params + const { workspaceSlug } = useParams(); + const pathname = usePathname(); + // store hooks + const { allowPermissions } = useUserPermissions(); + // translation + const { t } = useTranslation(); + + return ( +
+ {WORKSPACE_SETTINGS_CATEGORIES.map((category) => { + const categoryItems = GROUPED_WORKSPACE_SETTINGS[category]; + const accessibleItems = categoryItems.filter((item) => + allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug) + ); + + if (accessibleItems.length === 0) return null; + + return ( +
+
{t(category)}
+
+ {accessibleItems.map((item) => { + const isItemActive = + item.href === "/settings" + ? pathname === `/${workspaceSlug}${item.href}/` + : new RegExp(`^/${workspaceSlug}${item.href}/`).test(pathname); + + return ( + + ); + })} +
+
+ ); + })} +
+ ); +}); diff --git a/apps/web/core/components/settings/workspace/sidebar/item-icon.tsx b/apps/web/core/components/settings/workspace/sidebar/item-icon.tsx new file mode 100644 index 0000000000..3a34f969ba --- /dev/null +++ b/apps/web/core/components/settings/workspace/sidebar/item-icon.tsx @@ -0,0 +1,13 @@ +import type { LucideIcon } from "lucide-react"; +import { ArrowUpToLine, Building, CreditCard, Users, Webhook } from "lucide-react"; +// plane imports +import type { ISvgIcons } from "@plane/propel/icons"; +import type { TWorkspaceSettingsTabs } from "@plane/types"; + +export const WORKSPACE_SETTINGS_ICONS: Record> = { + general: Building, + members: Users, + export: ArrowUpToLine, + "billing-and-plans": CreditCard, + webhooks: Webhook, +}; diff --git a/apps/web/core/components/settings/workspace/sidebar/root.tsx b/apps/web/core/components/settings/workspace/sidebar/root.tsx new file mode 100644 index 0000000000..f3f1e10e00 --- /dev/null +++ b/apps/web/core/components/settings/workspace/sidebar/root.tsx @@ -0,0 +1,29 @@ +// plane imports +import { ScrollArea } from "@plane/propel/scrollarea"; +import { cn } from "@plane/utils"; +// local imports +import { WorkspaceSettingsSidebarHeader } from "./header"; +import { WorkspaceSettingsSidebarItemCategories } from "./item-categories"; + +type Props = { + className?: string; +}; + +export function WorkspaceSettingsSidebarRoot(props: Props) { + const { className } = props; + + return ( + + + + + ); +} diff --git a/apps/web/core/components/sidebar/sidebar-wrapper.tsx b/apps/web/core/components/sidebar/sidebar-wrapper.tsx index c6931444a1..28bc3d2845 100644 --- a/apps/web/core/components/sidebar/sidebar-wrapper.tsx +++ b/apps/web/core/components/sidebar/sidebar-wrapper.tsx @@ -44,7 +44,7 @@ export const SidebarWrapper = observer(function SidebarWrapper(props: TSidebarWr return ( <> setIsCustomizeNavDialogOpen(false)} /> -
+
{/* Workspace switcher and settings */} diff --git a/apps/web/core/components/web-hooks/webhooks-list-item.tsx b/apps/web/core/components/web-hooks/webhooks-list-item.tsx index 1355406f7a..3795d40e1d 100644 --- a/apps/web/core/components/web-hooks/webhooks-list-item.tsx +++ b/apps/web/core/components/web-hooks/webhooks-list-item.tsx @@ -23,12 +23,15 @@ export function WebhooksListItem(props: IWebhookListItem) { }; return ( -
- - -
{webhook.url}
+
+ +
{webhook.url}
+
- +
); diff --git a/apps/web/core/components/web-hooks/webhooks-list.tsx b/apps/web/core/components/web-hooks/webhooks-list.tsx index fe21e209da..62043596c5 100644 --- a/apps/web/core/components/web-hooks/webhooks-list.tsx +++ b/apps/web/core/components/web-hooks/webhooks-list.tsx @@ -9,7 +9,7 @@ export const WebhooksList = observer(function WebhooksList() { const { webhooks } = useWebhook(); return ( -
+
{Object.values(webhooks ?? {}).map((webhook) => ( ))} diff --git a/apps/web/core/components/workspace/billing/comparison/base.tsx b/apps/web/core/components/workspace/billing/comparison/base.tsx index 4ab5576a56..f198907f00 100644 --- a/apps/web/core/components/workspace/billing/comparison/base.tsx +++ b/apps/web/core/components/workspace/billing/comparison/base.tsx @@ -35,7 +35,7 @@ export const PlansComparisonBase = observer(function PlansComparisonBase(props: const getSubscriptionType = (planKey: TPlanePlans) => planDetails[planKey].id; return ( -
+
diff --git a/apps/web/core/components/workspace/settings/members-list.tsx b/apps/web/core/components/workspace/settings/members-list.tsx index f1560c143f..1dae187643 100644 --- a/apps/web/core/components/workspace/settings/members-list.tsx +++ b/apps/web/core/components/workspace/settings/members-list.tsx @@ -66,7 +66,7 @@ export const WorkspaceMembersList = observer(function WorkspaceMembersList(props return ( <> -
+
{searchedMemberIds?.length !== 0 && } {searchedInvitationsIds?.length === 0 && searchedMemberIds?.length === 0 && (

{t("no_matching_members")}

diff --git a/apps/web/core/components/workspace/settings/workspace-details.tsx b/apps/web/core/components/workspace/settings/workspace-details.tsx index 5bcc832d2d..66cb59c720 100644 --- a/apps/web/core/components/workspace/settings/workspace-details.tsx +++ b/apps/web/core/components/workspace/settings/workspace-details.tsx @@ -9,7 +9,7 @@ import { EditIcon } from "@plane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { IWorkspace } from "@plane/types"; import { CustomSelect, Input } from "@plane/ui"; -import { copyUrlToClipboard, getFileURL } from "@plane/utils"; +import { cn, copyUrlToClipboard, getFileURL } from "@plane/utils"; // components import { WorkspaceImageUploadModal } from "@/components/core/modals/workspace-image-upload-modal"; import { TimezoneSelect } from "@/components/global/timezone-select"; @@ -119,7 +119,8 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() { const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); - if (!currentWorkspace) return <>; + if (!currentWorkspace) return null; + return ( <> )} /> -
-
-
+
+
+
{isAdmin && (
- -
+
-
-

{t("workspace_settings.settings.general.name")}

+
+

{t("workspace_settings.settings.general.name")}

- -
-

+
+

{t("workspace_settings.settings.general.company_size")}

- -
-

{t("workspace_settings.settings.general.url")}

+
+

{t("workspace_settings.settings.general.url")}

- -
-

+
+

{t("workspace_settings.settings.general.workspace_timezone")}

( <> - + )} />

- - {isAdmin && ( -
- -
- )}
- {isAdmin && } + {isAdmin && ( +
+ +
+ )}

+ {isAdmin && ( +
+ +
+ )} ); }); diff --git a/apps/web/core/components/workspace/sidebar/user-menu-root.tsx b/apps/web/core/components/workspace/sidebar/user-menu-root.tsx index 2183492c53..8fbd13a2d5 100644 --- a/apps/web/core/components/workspace/sidebar/user-menu-root.tsx +++ b/apps/web/core/components/workspace/sidebar/user-menu-root.tsx @@ -1,7 +1,6 @@ import { useState, useEffect } from "react"; import { observer } from "mobx-react"; -import { useParams, useRouter } from "next/navigation"; -// icons +import { useRouter } from "next/navigation"; import { LogOut, Settings, Settings2 } from "lucide-react"; // plane imports import { GOD_MODE_URL } from "@plane/constants"; @@ -9,33 +8,31 @@ import { useTranslation } from "@plane/i18n"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { Avatar, CustomMenu } from "@plane/ui"; import { getFileURL } from "@plane/utils"; -// hooks +// components +import { CoverImage } from "@/components/common/cover-image"; import { AppSidebarItem } from "@/components/sidebar/sidebar-item"; +// hooks import { useAppTheme } from "@/hooks/store/use-app-theme"; +import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useUser } from "@/hooks/store/user"; -type Props = { - size?: "xs" | "sm" | "md"; -}; - -export const UserMenuRoot = observer(function UserMenuRoot(props: Props) { - const { size = "sm" } = props; - const { workspaceSlug } = useParams(); +export const UserMenuRoot = observer(function UserMenuRoot() { + // states + const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); // router const router = useRouter(); // store hooks const { toggleAnySidebarDropdown } = useAppTheme(); const { data: currentUser } = useUser(); const { signOut } = useUser(); + const { toggleProfileSettingsModal } = useCommandPalette(); // derived values const isUserInstanceAdmin = false; // translation const { t } = useTranslation(); - // local state - const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); - const handleSignOut = async () => { - await signOut().catch(() => + const handleSignOut = () => { + signOut().catch(() => setToast({ type: TOAST_TYPE.ERROR, title: t("sign_out.toast.error.title"), @@ -48,7 +45,7 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: Props) { useEffect(() => { if (isUserMenuOpen) toggleAnySidebarDropdown(true); else toggleAnySidebarDropdown(false); - }, [isUserMenuOpen]); + }, [isUserMenuOpen, toggleAnySidebarDropdown]); return ( ), @@ -72,48 +69,75 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: Props) { menuButtonOnClick={() => !isUserMenuOpen && setIsUserMenuOpen(true)} onMenuClose={() => setIsUserMenuOpen(false)} placement="bottom-end" - maxHeight="lg" + maxHeight="2xl" + optionsClassName="w-72 p-3 flex flex-col gap-y-3" closeOnSelect > -
- {currentUser?.email} - router.push(`/${workspaceSlug}/settings/account`)}> -
- - {t("settings")} +
+ +
+
+
+
+ +
+
+

+ {currentUser?.first_name} {currentUser?.last_name} +

+

{currentUser?.email}

+
+
+
+
+ + toggleProfileSettingsModal({ + activeTab: "general", + isOpen: true, + }) + } + className="flex items-center gap-2" + > + + {t("settings")} - router.push(`/${workspaceSlug}/settings/account/preferences`)}> -
- - Preferences -
-
-
-
-
- - + + toggleProfileSettingsModal({ + activeTab: "preferences", + isOpen: true, + }) + } + className="flex items-center gap-2" + > + + {t("preferences")}
+ + + {t("sign_out")} + {isUserInstanceAdmin && ( - <> -
-
- router.push(GOD_MODE_URL)}> -
- {t("enter_god_mode")} -
-
-
- + router.push(GOD_MODE_URL)} + className="bg-accent-primary/20 text-accent-primary hover:bg-accent-primary/30 hover:text-accent-secondary" + > + {t("enter_god_mode")} + )} ); diff --git a/apps/web/core/layouts/auth-layout/workspace-wrapper.tsx b/apps/web/core/layouts/auth-layout/workspace-wrapper.tsx index 256afec7df..394fdabf38 100644 --- a/apps/web/core/layouts/auth-layout/workspace-wrapper.tsx +++ b/apps/web/core/layouts/auth-layout/workspace-wrapper.tsx @@ -178,15 +178,12 @@ export const WorkspaceAuthWrapper = observer(function WorkspaceAuthWrapper(props )} {allWorkspaces?.length > 0 && ( - + Visit Profile )} {allWorkspaces && allWorkspaces.length === 0 && ( - + Create new workspace )} diff --git a/apps/web/core/store/base-command-palette.store.ts b/apps/web/core/store/base-command-palette.store.ts index d48769f7e2..e459295303 100644 --- a/apps/web/core/store/base-command-palette.store.ts +++ b/apps/web/core/store/base-command-palette.store.ts @@ -1,8 +1,11 @@ -import { observable, action, makeObservable } from "mobx"; +import { observable, action, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; +// plane imports import type { TCreateModalStoreTypes, TCreatePageModal } from "@plane/constants"; import { DEFAULT_CREATE_PAGE_MODAL_DATA, EPageAccess } from "@plane/constants"; +import type { TProfileSettingsTabs } from "@plane/types"; import { EIssuesStoreType } from "@plane/types"; +// lib import { store } from "@/lib/store-context"; export interface ModalData { @@ -22,6 +25,10 @@ export interface IBaseCommandPaletteStore { isBulkDeleteIssueModalOpen: boolean; createIssueStoreType: TCreateModalStoreTypes; createWorkItemAllowedProjectIds: string[] | undefined; + profileSettingsModal: { + activeTab: TProfileSettingsTabs | null; + isOpen: boolean; + }; allStickiesModal: boolean; projectListOpenMap: Record; getIsProjectListOpen: (projectId: string) => boolean; @@ -36,6 +43,7 @@ export interface IBaseCommandPaletteStore { toggleBulkDeleteIssueModal: (value?: boolean) => void; toggleAllStickiesModal: (value?: boolean) => void; toggleProjectListOpen: (projectId: string, value?: boolean) => void; + toggleProfileSettingsModal: (value: { activeTab?: TProfileSettingsTabs | null; isOpen?: boolean }) => void; } export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStore { @@ -50,6 +58,10 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor createPageModal: TCreatePageModal = DEFAULT_CREATE_PAGE_MODAL_DATA; createIssueStoreType: TCreateModalStoreTypes = EIssuesStoreType.PROJECT; createWorkItemAllowedProjectIds: IBaseCommandPaletteStore["createWorkItemAllowedProjectIds"] = undefined; + profileSettingsModal: IBaseCommandPaletteStore["profileSettingsModal"] = { + activeTab: "general", + isOpen: false, + }; allStickiesModal: boolean = false; projectListOpenMap: Record = {}; @@ -66,6 +78,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor createPageModal: observable, createIssueStoreType: observable, createWorkItemAllowedProjectIds: observable, + profileSettingsModal: observable, allStickiesModal: observable, projectListOpenMap: observable, // toggle actions @@ -79,6 +92,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor toggleBulkDeleteIssueModal: action, toggleAllStickiesModal: action, toggleProjectListOpen: action, + toggleProfileSettingsModal: action, }); } @@ -240,4 +254,20 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor this.allStickiesModal = !this.allStickiesModal; } }; + + /** + * Toggles the profile settings modal + * @param value + * @returns + */ + toggleProfileSettingsModal: IBaseCommandPaletteStore["toggleProfileSettingsModal"] = (payload) => { + const updatedSettings: IBaseCommandPaletteStore["profileSettingsModal"] = { + ...this.profileSettingsModal, + ...payload, + }; + + runInAction(() => { + this.profileSettingsModal = updatedSettings; + }); + }; } diff --git a/apps/web/ee/constants/project/settings/tabs.ts b/apps/web/ee/constants/project/settings/tabs.ts deleted file mode 100644 index 0f004a8325..0000000000 --- a/apps/web/ee/constants/project/settings/tabs.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "ce/constants/project/settings/tabs"; diff --git a/packages/constants/src/profile.ts b/packages/constants/src/profile.ts index cb90d62d3b..1dfa3c41a0 100644 --- a/packages/constants/src/profile.ts +++ b/packages/constants/src/profile.ts @@ -1,57 +1,6 @@ +// plane imports import { EStartOfTheWeek } from "@plane/types"; -export const PROFILE_SETTINGS = { - profile: { - key: "profile", - i18n_label: "profile.actions.profile", - href: `/settings/account`, - highlight: (pathname: string) => pathname === "/settings/account/", - }, - security: { - key: "security", - i18n_label: "profile.actions.security", - href: `/settings/account/security`, - highlight: (pathname: string) => pathname === "/settings/account/security/", - }, - activity: { - key: "activity", - i18n_label: "profile.actions.activity", - href: `/settings/account/activity`, - highlight: (pathname: string) => pathname === "/settings/account/activity/", - }, - preferences: { - key: "preferences", - i18n_label: "profile.actions.preferences", - href: `/settings/account/preferences`, - highlight: (pathname: string) => pathname === "/settings/account/preferences", - }, - notifications: { - key: "notifications", - i18n_label: "profile.actions.notifications", - href: `/settings/account/notifications`, - highlight: (pathname: string) => pathname === "/settings/account/notifications/", - }, - "api-tokens": { - key: "api-tokens", - i18n_label: "profile.actions.api-tokens", - href: `/settings/account/api-tokens`, - highlight: (pathname: string) => pathname === "/settings/account/api-tokens/", - }, -}; -export const PROFILE_ACTION_LINKS: { - key: string; - i18n_label: string; - href: string; - highlight: (pathname: string) => boolean; -}[] = [ - PROFILE_SETTINGS["profile"], - PROFILE_SETTINGS["security"], - PROFILE_SETTINGS["activity"], - PROFILE_SETTINGS["preferences"], - PROFILE_SETTINGS["notifications"], - PROFILE_SETTINGS["api-tokens"], -]; - export const PROFILE_VIEWER_TAB = [ { key: "summary", @@ -98,11 +47,6 @@ export const PREFERENCE_OPTIONS: { title: "theme", description: "select_or_customize_your_interface_color_scheme", }, - { - id: "start_of_week", - title: "First day of the week", - description: "This will change how all calendars in your app look.", - }, ]; /** diff --git a/packages/constants/src/settings.ts b/packages/constants/src/settings.ts deleted file mode 100644 index 2c55a6a2dd..0000000000 --- a/packages/constants/src/settings.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { PROFILE_SETTINGS } from "./profile"; -import { WORKSPACE_SETTINGS } from "./workspace"; - -export enum WORKSPACE_SETTINGS_CATEGORY { - ADMINISTRATION = "administration", - FEATURES = "features", - DEVELOPER = "developer", -} - -export enum PROFILE_SETTINGS_CATEGORY { - YOUR_PROFILE = "your profile", - DEVELOPER = "developer", -} - -export enum PROJECT_SETTINGS_CATEGORY { - PROJECTS = "projects", -} - -export const WORKSPACE_SETTINGS_CATEGORIES = [ - WORKSPACE_SETTINGS_CATEGORY.ADMINISTRATION, - WORKSPACE_SETTINGS_CATEGORY.FEATURES, - WORKSPACE_SETTINGS_CATEGORY.DEVELOPER, -]; - -export const PROFILE_SETTINGS_CATEGORIES = [ - PROFILE_SETTINGS_CATEGORY.YOUR_PROFILE, - PROFILE_SETTINGS_CATEGORY.DEVELOPER, -]; - -export const PROJECT_SETTINGS_CATEGORIES = [PROJECT_SETTINGS_CATEGORY.PROJECTS]; - -export const GROUPED_WORKSPACE_SETTINGS = { - [WORKSPACE_SETTINGS_CATEGORY.ADMINISTRATION]: [ - WORKSPACE_SETTINGS["general"], - WORKSPACE_SETTINGS["members"], - WORKSPACE_SETTINGS["billing-and-plans"], - WORKSPACE_SETTINGS["export"], - ], - [WORKSPACE_SETTINGS_CATEGORY.FEATURES]: [], - [WORKSPACE_SETTINGS_CATEGORY.DEVELOPER]: [WORKSPACE_SETTINGS["webhooks"]], -}; - -export const GROUPED_PROFILE_SETTINGS = { - [PROFILE_SETTINGS_CATEGORY.YOUR_PROFILE]: [ - PROFILE_SETTINGS["profile"], - PROFILE_SETTINGS["preferences"], - PROFILE_SETTINGS["notifications"], - PROFILE_SETTINGS["security"], - PROFILE_SETTINGS["activity"], - ], - [PROFILE_SETTINGS_CATEGORY.DEVELOPER]: [PROFILE_SETTINGS["api-tokens"]], -}; diff --git a/packages/constants/src/settings/index.ts b/packages/constants/src/settings/index.ts new file mode 100644 index 0000000000..e24ff3c651 --- /dev/null +++ b/packages/constants/src/settings/index.ts @@ -0,0 +1,3 @@ +export * from "./profile"; +export * from "./project"; +export * from "./workspace"; diff --git a/packages/constants/src/settings/profile.ts b/packages/constants/src/settings/profile.ts new file mode 100644 index 0000000000..ea5ed5a6fe --- /dev/null +++ b/packages/constants/src/settings/profile.ts @@ -0,0 +1,61 @@ +// plane imports +import type { TProfileSettingsTabs } from "@plane/types"; + +export enum PROFILE_SETTINGS_CATEGORY { + YOUR_PROFILE = "your profile", + DEVELOPER = "developer", +} + +export const PROFILE_SETTINGS_CATEGORIES: PROFILE_SETTINGS_CATEGORY[] = [ + PROFILE_SETTINGS_CATEGORY.YOUR_PROFILE, + PROFILE_SETTINGS_CATEGORY.DEVELOPER, +]; + +export const PROFILE_SETTINGS: Record< + TProfileSettingsTabs, + { + key: TProfileSettingsTabs; + i18n_label: string; + } +> = { + general: { + key: "general", + i18n_label: "profile.actions.profile", + }, + security: { + key: "security", + i18n_label: "profile.actions.security", + }, + activity: { + key: "activity", + i18n_label: "profile.actions.activity", + }, + preferences: { + key: "preferences", + i18n_label: "profile.actions.preferences", + }, + notifications: { + key: "notifications", + i18n_label: "profile.actions.notifications", + }, + "api-tokens": { + key: "api-tokens", + i18n_label: "profile.actions.api-tokens", + }, +}; + +export const PROFILE_SETTINGS_TABS: TProfileSettingsTabs[] = Object.keys(PROFILE_SETTINGS) as TProfileSettingsTabs[]; + +export const GROUPED_PROFILE_SETTINGS: Record< + PROFILE_SETTINGS_CATEGORY, + { key: TProfileSettingsTabs; i18n_label: string }[] +> = { + [PROFILE_SETTINGS_CATEGORY.YOUR_PROFILE]: [ + PROFILE_SETTINGS["general"], + PROFILE_SETTINGS["preferences"], + PROFILE_SETTINGS["notifications"], + PROFILE_SETTINGS["security"], + PROFILE_SETTINGS["activity"], + ], + [PROFILE_SETTINGS_CATEGORY.DEVELOPER]: [PROFILE_SETTINGS["api-tokens"]], +}; diff --git a/packages/constants/src/settings/project.ts b/packages/constants/src/settings/project.ts new file mode 100644 index 0000000000..7704c22fd2 --- /dev/null +++ b/packages/constants/src/settings/project.ts @@ -0,0 +1,116 @@ +// plane imports +import { EUserProjectRoles } from "@plane/types"; +import type { TProjectSettingsItem, TProjectSettingsTabs } from "@plane/types"; + +export enum PROJECT_SETTINGS_CATEGORY { + GENERAL = "general", + FEATURES = "features", + WORK_STRUCTURE = "work-structure", + EXECUTION = "execution", +} + +export const PROJECT_SETTINGS_CATEGORIES: PROJECT_SETTINGS_CATEGORY[] = [ + PROJECT_SETTINGS_CATEGORY.GENERAL, + PROJECT_SETTINGS_CATEGORY.FEATURES, + PROJECT_SETTINGS_CATEGORY.WORK_STRUCTURE, + PROJECT_SETTINGS_CATEGORY.EXECUTION, +]; + +export const PROJECT_SETTINGS: Record = { + general: { + key: "general", + i18n_label: "common.general", + href: ``, + access: [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER, EUserProjectRoles.GUEST], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/`, + }, + members: { + key: "members", + i18n_label: "common.members", + href: `/members`, + access: [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER, EUserProjectRoles.GUEST], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/members/`, + }, + features_cycles: { + key: "features_cycles", + i18n_label: "project_settings.features.cycles.short_title", + href: `/features/cycles`, + access: [EUserProjectRoles.ADMIN], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/features/cycles/`, + }, + features_modules: { + key: "features_modules", + i18n_label: "project_settings.features.modules.short_title", + href: `/features/modules`, + access: [EUserProjectRoles.ADMIN], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/features/modules/`, + }, + features_views: { + key: "features_views", + i18n_label: "project_settings.features.views.short_title", + href: `/features/views`, + access: [EUserProjectRoles.ADMIN], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/features/views/`, + }, + features_pages: { + key: "features_pages", + i18n_label: "project_settings.features.pages.short_title", + href: `/features/pages`, + access: [EUserProjectRoles.ADMIN], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/features/pages/`, + }, + features_intake: { + key: "features_intake", + i18n_label: "project_settings.features.intake.short_title", + href: `/features/intake`, + access: [EUserProjectRoles.ADMIN], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/features/intake/`, + }, + states: { + key: "states", + i18n_label: "common.states", + href: `/states`, + access: [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/states/`, + }, + labels: { + key: "labels", + i18n_label: "common.labels", + href: `/labels`, + access: [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/labels/`, + }, + estimates: { + key: "estimates", + i18n_label: "common.estimates", + href: `/estimates`, + access: [EUserProjectRoles.ADMIN], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/estimates/`, + }, + automations: { + key: "automations", + i18n_label: "project_settings.automations.label", + href: `/automations`, + access: [EUserProjectRoles.ADMIN], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/automations/`, + }, +}; + +export const PROJECT_SETTINGS_FLAT_MAP: TProjectSettingsItem[] = Object.values(PROJECT_SETTINGS); + +export const GROUPED_PROJECT_SETTINGS: Record = { + [PROJECT_SETTINGS_CATEGORY.GENERAL]: [PROJECT_SETTINGS["general"], PROJECT_SETTINGS["members"]], + [PROJECT_SETTINGS_CATEGORY.FEATURES]: [ + PROJECT_SETTINGS["features_cycles"], + PROJECT_SETTINGS["features_modules"], + PROJECT_SETTINGS["features_views"], + PROJECT_SETTINGS["features_pages"], + PROJECT_SETTINGS["features_intake"], + ], + [PROJECT_SETTINGS_CATEGORY.WORK_STRUCTURE]: [ + PROJECT_SETTINGS["states"], + PROJECT_SETTINGS["labels"], + PROJECT_SETTINGS["estimates"], + ], + [PROJECT_SETTINGS_CATEGORY.EXECUTION]: [PROJECT_SETTINGS["automations"]], +}; diff --git a/packages/constants/src/settings/workspace.ts b/packages/constants/src/settings/workspace.ts new file mode 100644 index 0000000000..d56b989aaa --- /dev/null +++ b/packages/constants/src/settings/workspace.ts @@ -0,0 +1,68 @@ +// plane imports +import type { TWorkspaceSettingsItem, TWorkspaceSettingsTabs } from "@plane/types"; +import { EUserWorkspaceRoles } from "@plane/types"; + +export enum WORKSPACE_SETTINGS_CATEGORY { + ADMINISTRATION = "administration", + FEATURES = "features", + DEVELOPER = "developer", +} + +export const WORKSPACE_SETTINGS_CATEGORIES: WORKSPACE_SETTINGS_CATEGORY[] = [ + WORKSPACE_SETTINGS_CATEGORY.ADMINISTRATION, + WORKSPACE_SETTINGS_CATEGORY.FEATURES, + WORKSPACE_SETTINGS_CATEGORY.DEVELOPER, +]; + +export const WORKSPACE_SETTINGS: Record = { + general: { + key: "general", + i18n_label: "workspace_settings.settings.general.title", + href: `/settings`, + access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`, + }, + members: { + key: "members", + i18n_label: "workspace_settings.settings.members.title", + href: `/settings/members`, + access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`, + }, + "billing-and-plans": { + key: "billing-and-plans", + i18n_label: "workspace_settings.settings.billing_and_plans.title", + href: `/settings/billing`, + access: [EUserWorkspaceRoles.ADMIN], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/billing/`, + }, + export: { + key: "export", + i18n_label: "workspace_settings.settings.exports.title", + href: `/settings/exports`, + access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`, + }, + webhooks: { + key: "webhooks", + i18n_label: "workspace_settings.settings.webhooks.title", + href: `/settings/webhooks`, + access: [EUserWorkspaceRoles.ADMIN], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/webhooks/`, + }, +}; + +export const WORKSPACE_SETTINGS_ACCESS = Object.fromEntries( + Object.entries(WORKSPACE_SETTINGS).map(([_, { href, access }]) => [href, access]) +); + +export const GROUPED_WORKSPACE_SETTINGS: Record = { + [WORKSPACE_SETTINGS_CATEGORY.ADMINISTRATION]: [ + WORKSPACE_SETTINGS["general"], + WORKSPACE_SETTINGS["members"], + WORKSPACE_SETTINGS["billing-and-plans"], + WORKSPACE_SETTINGS["export"], + ], + [WORKSPACE_SETTINGS_CATEGORY.FEATURES]: [], + [WORKSPACE_SETTINGS_CATEGORY.DEVELOPER]: [WORKSPACE_SETTINGS["webhooks"]], +}; diff --git a/packages/constants/src/workspace.ts b/packages/constants/src/workspace.ts index 9610333c0e..f2cf5be972 100644 --- a/packages/constants/src/workspace.ts +++ b/packages/constants/src/workspace.ts @@ -1,9 +1,9 @@ import type { TStaticViewTypes, IWorkspaceSearchResults } from "@plane/types"; import { EUserWorkspaceRoles } from "@plane/types"; -export const ORGANIZATION_SIZE = ["Just myself", "2-10", "11-50", "51-200", "201-500", "500+"]; +export const ORGANIZATION_SIZE: string[] = ["Just myself", "2-10", "11-50", "51-200", "201-500", "500+"]; -export const RESTRICTED_URLS = [ +export const RESTRICTED_URLS: string[] = [ "404", "accounts", "api", @@ -71,62 +71,6 @@ export const RESTRICTED_URLS = [ "instance", ]; -export const WORKSPACE_SETTINGS = { - general: { - key: "general", - i18n_label: "workspace_settings.settings.general.title", - href: `/settings`, - access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`, - }, - members: { - key: "members", - i18n_label: "workspace_settings.settings.members.title", - href: `/settings/members`, - access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`, - }, - "billing-and-plans": { - key: "billing-and-plans", - i18n_label: "workspace_settings.settings.billing_and_plans.title", - href: `/settings/billing`, - access: [EUserWorkspaceRoles.ADMIN], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/billing/`, - }, - export: { - key: "export", - i18n_label: "workspace_settings.settings.exports.title", - href: `/settings/exports`, - access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/exports/`, - }, - webhooks: { - key: "webhooks", - i18n_label: "workspace_settings.settings.webhooks.title", - href: `/settings/webhooks`, - access: [EUserWorkspaceRoles.ADMIN], - highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/webhooks/`, - }, -}; - -export const WORKSPACE_SETTINGS_ACCESS = Object.fromEntries( - Object.entries(WORKSPACE_SETTINGS).map(([_, { href, access }]) => [href, access]) -); - -export const WORKSPACE_SETTINGS_LINKS: { - key: string; - i18n_label: string; - href: string; - access: EUserWorkspaceRoles[]; - highlight: (pathname: string, baseUrl: string) => boolean; -}[] = [ - WORKSPACE_SETTINGS["general"], - WORKSPACE_SETTINGS["members"], - WORKSPACE_SETTINGS["billing-and-plans"], - WORKSPACE_SETTINGS["export"], - WORKSPACE_SETTINGS["webhooks"], -]; - export const ROLE = { [EUserWorkspaceRoles.GUEST]: "Guest", [EUserWorkspaceRoles.MEMBER]: "Member", diff --git a/packages/i18n/src/locales/cs/translations.ts b/packages/i18n/src/locales/cs/translations.ts index 3b98f60c20..87fe4f5d99 100644 --- a/packages/i18n/src/locales/cs/translations.ts +++ b/packages/i18n/src/locales/cs/translations.ts @@ -1966,6 +1966,44 @@ export default { primary_button: "Přidat systém odhadů", }, }, + features: { + cycles: { + title: "Cykly", + short_title: "Cykly", + description: + "Naplánujte práci v flexibilních obdobích, která se přizpůsobí jedinečnému rytmu a tempu tohoto projektu.", + toggle_title: "Povolit cykly", + toggle_description: "Naplánujte práci v soustředěných časových rámcích.", + }, + modules: { + title: "Moduly", + short_title: "Moduly", + description: "Organizujte práci do dílčích projektů s vyhrazenými vedoucími a přiřazenými osobami.", + toggle_title: "Povolit moduly", + toggle_description: "Členové projektu budou moci vytvářet a upravovat moduly.", + }, + views: { + title: "Zobrazení", + short_title: "Zobrazení", + description: "Uložte vlastní řazení, filtry a možnosti zobrazení nebo je sdílejte se svým týmem.", + toggle_title: "Povolit zobrazení", + toggle_description: "Členové projektu budou moci vytvářet a upravovat zobrazení.", + }, + pages: { + title: "Stránky", + short_title: "Stránky", + description: "Vytvářejte a upravujte volný obsah: poznámky, dokumenty, cokoliv.", + toggle_title: "Povolit stránky", + toggle_description: "Členové projektu budou moci vytvářet a upravovat stránky.", + }, + intake: { + title: "Příjem", + short_title: "Příjem", + description: "Umožněte nečlenům sdílet chyby, zpětnou vazbu a návrhy; bez narušení vašeho pracovního postupu.", + toggle_title: "Povolit příjem", + toggle_description: "Povolit členům projektu vytvářet žádosti o příjem v aplikaci.", + }, + }, }, project_cycles: { add_cycle: "Přidat cyklus", diff --git a/packages/i18n/src/locales/de/translations.ts b/packages/i18n/src/locales/de/translations.ts index d6b5a7d417..a9e676ec1f 100644 --- a/packages/i18n/src/locales/de/translations.ts +++ b/packages/i18n/src/locales/de/translations.ts @@ -1990,6 +1990,46 @@ export default { primary_button: "Schätzungssystem hinzufügen", }, }, + features: { + cycles: { + title: "Zyklen", + short_title: "Zyklen", + description: + "Planen Sie die Arbeit in flexiblen Zeiträumen, die sich dem einzigartigen Rhythmus und Tempo dieses Projekts anpassen.", + toggle_title: "Zyklen aktivieren", + toggle_description: "Planen Sie die Arbeit in fokussierten Zeiträumen.", + }, + modules: { + title: "Module", + short_title: "Module", + description: "Organisieren Sie die Arbeit in Teilprojekte mit engagierten Leitern und Verantwortlichen.", + toggle_title: "Module aktivieren", + toggle_description: "Projektmitglieder können Module erstellen und bearbeiten.", + }, + views: { + title: "Ansichten", + short_title: "Ansichten", + description: + "Speichern Sie benutzerdefinierte Sortierungen, Filter und Anzeigeoptionen oder teilen Sie sie mit Ihrem Team.", + toggle_title: "Ansichten aktivieren", + toggle_description: "Projektmitglieder können Ansichten erstellen und bearbeiten.", + }, + pages: { + title: "Seiten", + short_title: "Seiten", + description: "Erstellen und bearbeiten Sie freie Inhalte: Notizen, Dokumente, alles.", + toggle_title: "Seiten aktivieren", + toggle_description: "Projektmitglieder können Seiten erstellen und bearbeiten.", + }, + intake: { + title: "Aufnahme", + short_title: "Aufnahme", + description: + "Ermöglichen Sie Nicht-Mitgliedern, Fehler, Feedback und Vorschläge zu teilen, ohne Ihren Workflow zu unterbrechen.", + toggle_title: "Aufnahme aktivieren", + toggle_description: "Projektmitgliedern erlauben, In-App-Aufnahmeanfragen zu erstellen.", + }, + }, }, project_cycles: { add_cycle: "Zyklus hinzufügen", diff --git a/packages/i18n/src/locales/en/translations.ts b/packages/i18n/src/locales/en/translations.ts index d41e7ecb06..c45990182f 100644 --- a/packages/i18n/src/locales/en/translations.ts +++ b/packages/i18n/src/locales/en/translations.ts @@ -1843,6 +1843,43 @@ export default { primary_button: "Add estimate system", }, }, + features: { + cycles: { + title: "Cycles", + short_title: "Cycles", + description: "Schedule work in flexible periods that adapt to this project's unique rhythm and pace.", + toggle_title: "Enable cycles", + toggle_description: "Plan work in focused timeframes.", + }, + modules: { + title: "Modules", + short_title: "Modules", + description: "Organize work into sub-projects with dedicated leads and assignees.", + toggle_title: "Enable modules", + toggle_description: "Project members will be able to create and edit modules.", + }, + views: { + title: "Views", + short_title: "Views", + description: "Save custom sorts, filters, and display options or share them with your team.", + toggle_title: "Enable views", + toggle_description: "Project members will be able to create and edit views.", + }, + pages: { + title: "Pages", + short_title: "Pages", + description: "Create and edit free-form content; notes, docs, anything.", + toggle_title: "Enable pages", + toggle_description: "Project members will be able to create and edit pages.", + }, + intake: { + title: "Intake", + short_title: "Intake", + description: "Let non-members share bugs, feedback, and suggestions; without disrupting your workflow.", + toggle_title: "Enable intake", + toggle_description: "Let project members create in app intake requests.", + }, + }, }, project_cycles: { add_cycle: "Add cycle", diff --git a/packages/i18n/src/locales/es/translations.ts b/packages/i18n/src/locales/es/translations.ts index 85807f743f..302e4b1eb0 100644 --- a/packages/i18n/src/locales/es/translations.ts +++ b/packages/i18n/src/locales/es/translations.ts @@ -1999,6 +1999,46 @@ export default { primary_button: "Agregar sistema de estimación", }, }, + features: { + cycles: { + title: "Ciclos", + short_title: "Ciclos", + description: + "Programa el trabajo en períodos flexibles que se adaptan al ritmo y al tempo únicos de este proyecto.", + toggle_title: "Habilitar ciclos", + toggle_description: "Planifica el trabajo en períodos de tiempo enfocados.", + }, + modules: { + title: "Módulos", + short_title: "Módulos", + description: "Organiza el trabajo en subproyectos con líderes y responsables dedicados.", + toggle_title: "Habilitar módulos", + toggle_description: "Los miembros del proyecto podrán crear y editar módulos.", + }, + views: { + title: "Vistas", + short_title: "Vistas", + description: + "Guarda ordenaciones, filtros y opciones de visualización personalizadas o compártelos con tu equipo.", + toggle_title: "Habilitar vistas", + toggle_description: "Los miembros del proyecto podrán crear y editar vistas.", + }, + pages: { + title: "Páginas", + short_title: "Páginas", + description: "Crea y edita contenido libre: notas, documentos, cualquier cosa.", + toggle_title: "Habilitar páginas", + toggle_description: "Los miembros del proyecto podrán crear y editar páginas.", + }, + intake: { + title: "Recepción", + short_title: "Recepción", + description: + "Permite que los no miembros compartan errores, comentarios y sugerencias; sin interrumpir tu flujo de trabajo.", + toggle_title: "Habilitar recepción", + toggle_description: "Permitir a los miembros del proyecto crear solicitudes de recepción en la aplicación.", + }, + }, }, project_cycles: { add_cycle: "Agregar ciclo", diff --git a/packages/i18n/src/locales/fr/translations.ts b/packages/i18n/src/locales/fr/translations.ts index 79673abf0e..39feafad75 100644 --- a/packages/i18n/src/locales/fr/translations.ts +++ b/packages/i18n/src/locales/fr/translations.ts @@ -1997,6 +1997,46 @@ export default { primary_button: "Ajouter un système d’estimation", }, }, + features: { + cycles: { + title: "Cycles", + short_title: "Cycles", + description: + "Planifiez le travail dans des périodes flexibles qui s'adaptent au rythme et au tempo uniques de ce projet.", + toggle_title: "Activer les cycles", + toggle_description: "Planifiez le travail dans des périodes ciblées.", + }, + modules: { + title: "Modules", + short_title: "Modules", + description: "Organisez le travail en sous-projets avec des chefs de projet et des responsables dédiés.", + toggle_title: "Activer les modules", + toggle_description: "Les membres du projet pourront créer et modifier des modules.", + }, + views: { + title: "Vues", + short_title: "Vues", + description: + "Enregistrez des tris, des filtres et des options d'affichage personnalisés ou partagez-les avec votre équipe.", + toggle_title: "Activer les vues", + toggle_description: "Les membres du projet pourront créer et modifier des vues.", + }, + pages: { + title: "Pages", + short_title: "Pages", + description: "Créez et modifiez du contenu libre : notes, documents, n'importe quoi.", + toggle_title: "Activer les pages", + toggle_description: "Les membres du projet pourront créer et modifier des pages.", + }, + intake: { + title: "Réception", + short_title: "Réception", + description: + "Permettez aux non-membres de partager des bugs, des commentaires et des suggestions ; sans perturber votre flux de travail.", + toggle_title: "Activer la réception", + toggle_description: "Permettre aux membres du projet de créer des demandes de réception dans l'application.", + }, + }, }, project_cycles: { add_cycle: "Ajouter un cycle", diff --git a/packages/i18n/src/locales/id/translations.ts b/packages/i18n/src/locales/id/translations.ts index f49d4e8d25..f6edba7800 100644 --- a/packages/i18n/src/locales/id/translations.ts +++ b/packages/i18n/src/locales/id/translations.ts @@ -1982,6 +1982,44 @@ export default { primary_button: "Tambah sistem perkiraan", }, }, + features: { + cycles: { + title: "Siklus", + short_title: "Siklus", + description: + "Jadwalkan pekerjaan dalam periode fleksibel yang menyesuaikan dengan ritme dan tempo unik proyek ini.", + toggle_title: "Aktifkan siklus", + toggle_description: "Rencanakan pekerjaan dalam jangka waktu yang terfokus.", + }, + modules: { + title: "Modul", + short_title: "Modul", + description: "Atur pekerjaan menjadi sub-proyek dengan pemimpin dan penerima tugas khusus.", + toggle_title: "Aktifkan modul", + toggle_description: "Anggota proyek akan dapat membuat dan mengedit modul.", + }, + views: { + title: "Tampilan", + short_title: "Tampilan", + description: "Simpan pengurutan, filter, dan opsi tampilan kustom atau bagikan dengan tim Anda.", + toggle_title: "Aktifkan tampilan", + toggle_description: "Anggota proyek akan dapat membuat dan mengedit tampilan.", + }, + pages: { + title: "Halaman", + short_title: "Halaman", + description: "Buat dan edit konten bebas: catatan, dokumen, apa saja.", + toggle_title: "Aktifkan halaman", + toggle_description: "Anggota proyek akan dapat membuat dan mengedit halaman.", + }, + intake: { + title: "Penerimaan", + short_title: "Penerimaan", + description: "Biarkan non-anggota berbagi bug, umpan balik, dan saran; tanpa mengganggu alur kerja Anda.", + toggle_title: "Aktifkan penerimaan", + toggle_description: "Izinkan anggota proyek membuat permintaan penerimaan dalam aplikasi.", + }, + }, }, project_cycles: { add_cycle: "Tambah siklus", diff --git a/packages/i18n/src/locales/it/translations.ts b/packages/i18n/src/locales/it/translations.ts index 0506e30693..21d8dfb4f4 100644 --- a/packages/i18n/src/locales/it/translations.ts +++ b/packages/i18n/src/locales/it/translations.ts @@ -1986,6 +1986,46 @@ export default { primary_button: "Aggiungi sistema di stime", }, }, + features: { + cycles: { + title: "Cicli", + short_title: "Cicli", + description: + "Pianifica il lavoro in periodi flessibili che si adattano al ritmo e al tempo unici di questo progetto.", + toggle_title: "Abilita cicli", + toggle_description: "Pianifica il lavoro in periodi di tempo mirati.", + }, + modules: { + title: "Moduli", + short_title: "Moduli", + description: "Organizza il lavoro in sotto-progetti con responsabili e assegnatari dedicati.", + toggle_title: "Abilita moduli", + toggle_description: "I membri del progetto potranno creare e modificare moduli.", + }, + views: { + title: "Viste", + short_title: "Viste", + description: + "Salva ordinamenti, filtri e opzioni di visualizzazione personalizzati o condividili con il tuo team.", + toggle_title: "Abilita viste", + toggle_description: "I membri del progetto potranno creare e modificare viste.", + }, + pages: { + title: "Pagine", + short_title: "Pagine", + description: "Crea e modifica contenuti liberi: note, documenti, qualsiasi cosa.", + toggle_title: "Abilita pagine", + toggle_description: "I membri del progetto potranno creare e modificare pagine.", + }, + intake: { + title: "Ricezione", + short_title: "Ricezione", + description: + "Consenti ai non membri di condividere bug, feedback e suggerimenti; senza interrompere il tuo flusso di lavoro.", + toggle_title: "Abilita ricezione", + toggle_description: "Consenti ai membri del progetto di creare richieste di ricezione nell'app.", + }, + }, }, project_cycles: { add_cycle: "Aggiungi ciclo", diff --git a/packages/i18n/src/locales/ja/translations.ts b/packages/i18n/src/locales/ja/translations.ts index 80ae283c79..611218e414 100644 --- a/packages/i18n/src/locales/ja/translations.ts +++ b/packages/i18n/src/locales/ja/translations.ts @@ -1971,6 +1971,43 @@ export default { primary_button: "見積もりシステムを追加", }, }, + features: { + cycles: { + title: "サイクル", + short_title: "サイクル", + description: "このプロジェクト独自のリズムとペースに適応する柔軟な期間で作業をスケジュールします。", + toggle_title: "サイクルを有効にする", + toggle_description: "集中的な期間で作業を計画します。", + }, + modules: { + title: "モジュール", + short_title: "モジュール", + description: "専任のリーダーと担当者を持つサブプロジェクトに作業を整理します。", + toggle_title: "モジュールを有効にする", + toggle_description: "プロジェクトメンバーはモジュールを作成および編集できるようになります。", + }, + views: { + title: "ビュー", + short_title: "ビュー", + description: "カスタムソート、フィルター、表示オプションを保存したり、チームと共有したりします。", + toggle_title: "ビューを有効にする", + toggle_description: "プロジェクトメンバーはビューを作成および編集できるようになります。", + }, + pages: { + title: "ページ", + short_title: "ページ", + description: "自由形式のコンテンツを作成および編集します:メモ、ドキュメント、何でも。", + toggle_title: "ページを有効にする", + toggle_description: "プロジェクトメンバーはページを作成および編集できるようになります。", + }, + intake: { + title: "受付", + short_title: "受付", + description: "ワークフローを中断することなく、非メンバーがバグ、フィードバック、提案を共有できるようにします。", + toggle_title: "受付を有効にする", + toggle_description: "プロジェクトメンバーがアプリ内で受付リクエストを作成できるようにします。", + }, + }, }, project_cycles: { add_cycle: "サイクルを追加", diff --git a/packages/i18n/src/locales/ko/translations.ts b/packages/i18n/src/locales/ko/translations.ts index 11b6fa1c34..74dd37a3cb 100644 --- a/packages/i18n/src/locales/ko/translations.ts +++ b/packages/i18n/src/locales/ko/translations.ts @@ -1964,6 +1964,43 @@ export default { primary_button: "추정 시스템 추가", }, }, + features: { + cycles: { + title: "사이클", + short_title: "사이클", + description: "이 프로젝트의 고유한 리듬과 속도에 적응하는 유연한 기간으로 작업을 예약합니다.", + toggle_title: "사이클 활성화", + toggle_description: "집중된 기간에 작업을 계획합니다.", + }, + modules: { + title: "모듈", + short_title: "모듈", + description: "전담 리더와 담당자가 있는 하위 프로젝트로 작업을 구성합니다.", + toggle_title: "모듈 활성화", + toggle_description: "프로젝트 멤버가 모듈을 생성하고 편집할 수 있습니다.", + }, + views: { + title: "보기", + short_title: "보기", + description: "사용자 정의 정렬, 필터 및 표시 옵션을 저장하거나 팀과 공유합니다.", + toggle_title: "보기 활성화", + toggle_description: "프로젝트 멤버가 보기를 생성하고 편집할 수 있습니다.", + }, + pages: { + title: "페이지", + short_title: "페이지", + description: "자유 형식 콘텐츠를 생성하고 편집합니다: 메모, 문서, 무엇이든.", + toggle_title: "페이지 활성화", + toggle_description: "프로젝트 멤버가 페이지를 생성하고 편집할 수 있습니다.", + }, + intake: { + title: "접수", + short_title: "접수", + description: "워크플로를 방해하지 않고 비회원이 버그, 피드백 및 제안을 공유할 수 있도록 합니다.", + toggle_title: "접수 활성화", + toggle_description: "프로젝트 멤버가 앱 내에서 접수 요청을 생성할 수 있도록 허용합니다.", + }, + }, }, project_cycles: { add_cycle: "주기 추가", diff --git a/packages/i18n/src/locales/pl/translations.ts b/packages/i18n/src/locales/pl/translations.ts index be091cbcf5..b81dd7676e 100644 --- a/packages/i18n/src/locales/pl/translations.ts +++ b/packages/i18n/src/locales/pl/translations.ts @@ -1969,6 +1969,45 @@ export default { primary_button: "Dodaj system szacowania", }, }, + features: { + cycles: { + title: "Cykle", + short_title: "Cykle", + description: + "Planuj pracę w elastycznych okresach, które dostosowują się do unikalnego rytmu i tempa tego projektu.", + toggle_title: "Włącz cykle", + toggle_description: "Planuj pracę w skoncentrowanych ramach czasowych.", + }, + modules: { + title: "Moduły", + short_title: "Moduły", + description: "Organizuj pracę w podprojekty z dedykowanymi liderami i przypisanymi osobami.", + toggle_title: "Włącz moduły", + toggle_description: "Członkowie projektu będą mogli tworzyć i edytować moduły.", + }, + views: { + title: "Widoki", + short_title: "Widoki", + description: "Zapisuj niestandardowe sortowania, filtry i opcje wyświetlania lub udostępniaj je zespołowi.", + toggle_title: "Włącz widoki", + toggle_description: "Członkowie projektu będą mogli tworzyć i edytować widoki.", + }, + pages: { + title: "Strony", + short_title: "Strony", + description: "Twórz i edytuj dowolne treści: notatki, dokumenty, cokolwiek.", + toggle_title: "Włącz strony", + toggle_description: "Członkowie projektu będą mogli tworzyć i edytować strony.", + }, + intake: { + title: "Odbiór", + short_title: "Odbiór", + description: + "Pozwól osobom niebędącym członkami dzielić się błędami, opiniami i sugestiami; bez zakłócania przepływu pracy.", + toggle_title: "Włącz odbiór", + toggle_description: "Pozwól członkom projektu tworzyć żądania odbioru w aplikacji.", + }, + }, }, project_cycles: { add_cycle: "Dodaj cykl", diff --git a/packages/i18n/src/locales/pt-BR/translations.ts b/packages/i18n/src/locales/pt-BR/translations.ts index d926cdbe18..1424a438f7 100644 --- a/packages/i18n/src/locales/pt-BR/translations.ts +++ b/packages/i18n/src/locales/pt-BR/translations.ts @@ -1995,6 +1995,44 @@ export default { primary_button: "Adicionar sistema de estimativa", }, }, + features: { + cycles: { + title: "Ciclos", + short_title: "Ciclos", + description: "Agende o trabalho em períodos flexíveis que se adaptam ao ritmo e ao tempo únicos deste projeto.", + toggle_title: "Ativar ciclos", + toggle_description: "Planeje o trabalho em períodos de tempo focados.", + }, + modules: { + title: "Módulos", + short_title: "Módulos", + description: "Organize o trabalho em subprojetos com líderes e responsáveis dedicados.", + toggle_title: "Ativar módulos", + toggle_description: "Os membros do projeto poderão criar e editar módulos.", + }, + views: { + title: "Visualizações", + short_title: "Visualizações", + description: "Salve ordenações, filtros e opções de exibição personalizadas ou compartilhe-as com sua equipe.", + toggle_title: "Ativar visualizações", + toggle_description: "Os membros do projeto poderão criar e editar visualizações.", + }, + pages: { + title: "Páginas", + short_title: "Páginas", + description: "Crie e edite conteúdo livre: notas, documentos, qualquer coisa.", + toggle_title: "Ativar páginas", + toggle_description: "Os membros do projeto poderão criar e editar páginas.", + }, + intake: { + title: "Recepção", + short_title: "Recepção", + description: + "Permita que não membros compartilhem bugs, feedback e sugestões; sem interromper seu fluxo de trabalho.", + toggle_title: "Ativar recepção", + toggle_description: "Permitir que membros do projeto criem solicitações de recepção no aplicativo.", + }, + }, }, project_cycles: { add_cycle: "Adicionar ciclo", diff --git a/packages/i18n/src/locales/ro/translations.ts b/packages/i18n/src/locales/ro/translations.ts index fc4f043024..c9c748f7ff 100644 --- a/packages/i18n/src/locales/ro/translations.ts +++ b/packages/i18n/src/locales/ro/translations.ts @@ -1987,6 +1987,45 @@ export default { primary_button: "Adaugă sistem de estimare", }, }, + features: { + cycles: { + title: "Cicluri", + short_title: "Cicluri", + description: + "Programați munca în perioade flexibile care se adaptează ritmului și ritmului unic al acestui proiect.", + toggle_title: "Activați ciclurile", + toggle_description: "Planificați munca în intervale de timp concentrate.", + }, + modules: { + title: "Module", + short_title: "Module", + description: "Organizați munca în subproiecte cu lideri și responsabili dedicați.", + toggle_title: "Activați modulele", + toggle_description: "Membrii proiectului vor putea crea și edita module.", + }, + views: { + title: "Vizualizări", + short_title: "Vizualizări", + description: "Salvați sortări personalizate, filtre și opțiuni de afișare sau partajați-le cu echipa dvs.", + toggle_title: "Activați vizualizările", + toggle_description: "Membrii proiectului vor putea crea și edita vizualizări.", + }, + pages: { + title: "Pagini", + short_title: "Pagini", + description: "Creați și editați conținut liber: note, documente, orice.", + toggle_title: "Activați paginile", + toggle_description: "Membrii proiectului vor putea crea și edita pagini.", + }, + intake: { + title: "Recepție", + short_title: "Recepție", + description: + "Permiteți non-membrilor să partajeze erori, feedback și sugestii; fără a perturba fluxul de lucru.", + toggle_title: "Activați recepția", + toggle_description: "Permiteți membrilor proiectului să creeze solicitări de recepție în aplicație.", + }, + }, }, project_cycles: { add_cycle: "Adaugă ciclu", diff --git a/packages/i18n/src/locales/ru/translations.ts b/packages/i18n/src/locales/ru/translations.ts index ae823d17fe..f91403e914 100644 --- a/packages/i18n/src/locales/ru/translations.ts +++ b/packages/i18n/src/locales/ru/translations.ts @@ -1973,6 +1973,46 @@ export default { primary_button: "Добавить систему оценок", }, }, + features: { + cycles: { + title: "Циклы", + short_title: "Циклы", + description: + "Планируйте работу в гибких периодах, которые адаптируются к уникальному ритму и темпу этого проекта.", + toggle_title: "Включить циклы", + toggle_description: "Планируйте работу в целенаправленные периоды времени.", + }, + modules: { + title: "Модули", + short_title: "Модули", + description: "Организуйте работу в подпроекты с выделенными руководителями и исполнителями.", + toggle_title: "Включить модули", + toggle_description: "Участники проекта смогут создавать и редактировать модули.", + }, + views: { + title: "Представления", + short_title: "Представления", + description: + "Сохраняйте пользовательские сортировки, фильтры и параметры отображения или делитесь ими с командой.", + toggle_title: "Включить представления", + toggle_description: "Участники проекта смогут создавать и редактировать представления.", + }, + pages: { + title: "Страницы", + short_title: "Страницы", + description: "Создавайте и редактируйте свободный контент: заметки, документы, что угодно.", + toggle_title: "Включить страницы", + toggle_description: "Участники проекта смогут создавать и редактировать страницы.", + }, + intake: { + title: "Приём", + short_title: "Приём", + description: + "Позвольте не-участникам делиться ошибками, отзывами и предложениями; не нарушая ваш рабочий процесс.", + toggle_title: "Включить приём", + toggle_description: "Разрешить участникам проекта создавать запросы на приём в приложении.", + }, + }, }, project_cycles: { add_cycle: "Добавить цикл", diff --git a/packages/i18n/src/locales/sk/translations.ts b/packages/i18n/src/locales/sk/translations.ts index fb295de3f4..5e579e1d5f 100644 --- a/packages/i18n/src/locales/sk/translations.ts +++ b/packages/i18n/src/locales/sk/translations.ts @@ -1967,6 +1967,44 @@ export default { primary_button: "Pridať systém odhadov", }, }, + features: { + cycles: { + title: "Cykly", + short_title: "Cykly", + description: + "Naplánujte prácu v flexibilných obdobiach, ktoré sa prispôsobia jedinečnému rytmu a tempu tohto projektu.", + toggle_title: "Povoliť cykly", + toggle_description: "Naplánujte prácu v sústredenej časovej osi.", + }, + modules: { + title: "Moduly", + short_title: "Moduly", + description: "Organizujte prácu do podprojektov s vyčlenenými vedúcimi a priradenými osobami.", + toggle_title: "Povoliť moduly", + toggle_description: "Členovia projektu budú môcť vytvárať a upravovať moduly.", + }, + views: { + title: "Zobrazenia", + short_title: "Zobrazenia", + description: "Uložte vlastné triedenia, filtre a možnosti zobrazenia alebo ich zdieľajte so svojím tímom.", + toggle_title: "Povoliť zobrazenia", + toggle_description: "Členovia projektu budú môcť vytvárať a upravovať zobrazenia.", + }, + pages: { + title: "Stránky", + short_title: "Stránky", + description: "Vytvárajte a upravujte voľný obsah: poznámky, dokumenty, čokoľvek.", + toggle_title: "Povoliť stránky", + toggle_description: "Členovia projektu budú môcť vytvárať a upravovať stránky.", + }, + intake: { + title: "Príjem", + short_title: "Príjem", + description: "Umožnite nečlenom zdieľať chyby, spätnú väzbu a návrhy; bez narušenia vášho pracovného postupu.", + toggle_title: "Povoliť príjem", + toggle_description: "Povoliť členom projektu vytvárať žiadosti o príjem v aplikácii.", + }, + }, }, project_cycles: { add_cycle: "Pridať cyklus", diff --git a/packages/i18n/src/locales/tr-TR/translations.ts b/packages/i18n/src/locales/tr-TR/translations.ts index f7d987abb3..568856113a 100644 --- a/packages/i18n/src/locales/tr-TR/translations.ts +++ b/packages/i18n/src/locales/tr-TR/translations.ts @@ -1956,6 +1956,44 @@ export default { primary_button: "Tahmin sistemi ekle", }, }, + features: { + cycles: { + title: "Döngüler", + short_title: "Döngüler", + description: "Bu projenin benzersiz ritmine ve hızına uyum sağlayan esnek dönemlerde iş planlayın.", + toggle_title: "Döngüleri etkinleştir", + toggle_description: "Odaklanmış zaman dilimlerinde iş planlayın.", + }, + modules: { + title: "Modüller", + short_title: "Modüller", + description: "İşi özel liderler ve atananlarla alt projelere organize edin.", + toggle_title: "Modülleri etkinleştir", + toggle_description: "Proje üyeleri modüller oluşturabilir ve düzenleyebilir.", + }, + views: { + title: "Görünümler", + short_title: "Görünümler", + description: "Özel sıralamalar, filtreler ve görüntüleme seçeneklerini kaydedin veya ekibinizle paylaşın.", + toggle_title: "Görünümleri etkinleştir", + toggle_description: "Proje üyeleri görünümler oluşturabilir ve düzenleyebilir.", + }, + pages: { + title: "Sayfalar", + short_title: "Sayfalar", + description: "Serbest biçimli içerik oluşturun ve düzenleyin: notlar, belgeler, herhangi bir şey.", + toggle_title: "Sayfaları etkinleştir", + toggle_description: "Proje üyeleri sayfalar oluşturabilir ve düzenleyebilir.", + }, + intake: { + title: "Alım", + short_title: "Alım", + description: + "Üye olmayanların hataları, geri bildirimleri ve önerileri paylaşmasına izin verin; iş akışınızı aksatmadan.", + toggle_title: "Alımı etkinleştir", + toggle_description: "Proje üyelerinin uygulama içinde alım talepleri oluşturmasına izin verin.", + }, + }, }, project_cycles: { add_cycle: "Döngü ekle", diff --git a/packages/i18n/src/locales/ua/translations.ts b/packages/i18n/src/locales/ua/translations.ts index 04878fa054..2a56124272 100644 --- a/packages/i18n/src/locales/ua/translations.ts +++ b/packages/i18n/src/locales/ua/translations.ts @@ -1972,6 +1972,45 @@ export default { primary_button: "Додати систему оцінок", }, }, + features: { + cycles: { + title: "Цикли", + short_title: "Цикли", + description: "Плануйте роботу в гнучких періодах, які адаптуються до унікального ритму та темпу цього проекту.", + toggle_title: "Увімкнути цикли", + toggle_description: "Плануйте роботу в цілеспрямовані періоди часу.", + }, + modules: { + title: "Модулі", + short_title: "Модулі", + description: "Організуйте роботу в підпроекти з виділеними керівниками та виконавцями.", + toggle_title: "Увімкнути модулі", + toggle_description: "Учасники проекту зможуть створювати та редагувати модулі.", + }, + views: { + title: "Перегляди", + short_title: "Перегляди", + description: + "Зберігайте користувацькі сортування, фільтри та параметри відображення або діліться ними з командою.", + toggle_title: "Увімкнути перегляди", + toggle_description: "Учасники проекту зможуть створювати та редагувати перегляди.", + }, + pages: { + title: "Сторінки", + short_title: "Сторінки", + description: "Створюйте та редагуйте вільний контент: нотатки, документи, що завгодно.", + toggle_title: "Увімкнути сторінки", + toggle_description: "Учасники проекту зможуть створювати та редагувати сторінки.", + }, + intake: { + title: "Прийом", + short_title: "Прийом", + description: + "Дозвольте не-учасникам ділитися помилками, відгуками та пропозиціями; не порушуючи ваш робочий процес.", + toggle_title: "Увімкнути прийом", + toggle_description: "Дозволити учасникам проекту створювати запити на прийом в додатку.", + }, + }, }, project_cycles: { add_cycle: "Додати цикл", diff --git a/packages/i18n/src/locales/vi-VN/translations.ts b/packages/i18n/src/locales/vi-VN/translations.ts index c913d25b09..70a7cccded 100644 --- a/packages/i18n/src/locales/vi-VN/translations.ts +++ b/packages/i18n/src/locales/vi-VN/translations.ts @@ -1980,6 +1980,45 @@ export default { primary_button: "Thêm hệ thống ước tính", }, }, + features: { + cycles: { + title: "Chu kỳ", + short_title: "Chu kỳ", + description: + "Lên lịch công việc trong các khoảng thời gian linh hoạt thích ứng với nhịp điệu và tốc độ độc đáo của dự án này.", + toggle_title: "Bật chu kỳ", + toggle_description: "Lập kế hoạch công việc trong khung thời gian tập trung.", + }, + modules: { + title: "Mô-đun", + short_title: "Mô-đun", + description: "Tổ chức công việc thành các dự án phụ với người dẫn đầu và người được phân công chuyên trách.", + toggle_title: "Bật mô-đun", + toggle_description: "Thành viên dự án sẽ có thể tạo và chỉnh sửa mô-đun.", + }, + views: { + title: "Chế độ xem", + short_title: "Chế độ xem", + description: "Lưu các tùy chọn sắp xếp, bộ lọc và hiển thị tùy chỉnh hoặc chia sẻ chúng với nhóm của bạn.", + toggle_title: "Bật chế độ xem", + toggle_description: "Thành viên dự án sẽ có thể tạo và chỉnh sửa chế độ xem.", + }, + pages: { + title: "Trang", + short_title: "Trang", + description: "Tạo và chỉnh sửa nội dung tự do: ghi chú, tài liệu, bất cứ thứ gì.", + toggle_title: "Bật trang", + toggle_description: "Thành viên dự án sẽ có thể tạo và chỉnh sửa trang.", + }, + intake: { + title: "Tiếp nhận", + short_title: "Tiếp nhận", + description: + "Cho phép những người không phải thành viên chia sẻ lỗi, phản hồi và đề xuất; mà không làm gián đoạn quy trình làm việc của bạn.", + toggle_title: "Bật tiếp nhận", + toggle_description: "Cho phép thành viên dự án tạo yêu cầu tiếp nhận trong ứng dụng.", + }, + }, }, project_cycles: { add_cycle: "Thêm chu kỳ", diff --git a/packages/i18n/src/locales/zh-CN/translations.ts b/packages/i18n/src/locales/zh-CN/translations.ts index 4d2a520c2e..cca65f5bb5 100644 --- a/packages/i18n/src/locales/zh-CN/translations.ts +++ b/packages/i18n/src/locales/zh-CN/translations.ts @@ -1929,6 +1929,43 @@ export default { primary_button: "添加估算系统", }, }, + features: { + cycles: { + title: "周期", + short_title: "周期", + description: "在灵活的时间段内安排工作,以适应该项目独特的节奏和步调。", + toggle_title: "启用周期", + toggle_description: "在集中的时间段内规划工作。", + }, + modules: { + title: "模块", + short_title: "模块", + description: "将工作组织成具有专门负责人和受让人的子项目。", + toggle_title: "启用模块", + toggle_description: "项目成员将能够创建和编辑模块。", + }, + views: { + title: "视图", + short_title: "视图", + description: "保存自定义排序、过滤器和显示选项,或与团队共享。", + toggle_title: "启用视图", + toggle_description: "项目成员将能够创建和编辑视图。", + }, + pages: { + title: "页面", + short_title: "页面", + description: "创建和编辑自由格式的内容:笔记、文档、任何内容。", + toggle_title: "启用页面", + toggle_description: "项目成员将能够创建和编辑页面。", + }, + intake: { + title: "接收", + short_title: "接收", + description: "让非成员分享错误、反馈和建议;而不会中断您的工作流程。", + toggle_title: "启用接收", + toggle_description: "允许项目成员在应用中创建接收请求。", + }, + }, }, project_cycles: { add_cycle: "添加周期", diff --git a/packages/i18n/src/locales/zh-TW/translations.ts b/packages/i18n/src/locales/zh-TW/translations.ts index d63b1d06ee..337b329c76 100644 --- a/packages/i18n/src/locales/zh-TW/translations.ts +++ b/packages/i18n/src/locales/zh-TW/translations.ts @@ -1949,6 +1949,43 @@ export default { primary_button: "新增評估系統", }, }, + features: { + cycles: { + title: "週期", + short_title: "週期", + description: "在靈活的時間段內安排工作,以適應該專案獨特的節奏和步調。", + toggle_title: "啟用週期", + toggle_description: "在集中的時間段內規劃工作。", + }, + modules: { + title: "模組", + short_title: "模組", + description: "將工作組織成具有專門負責人和受讓人的子專案。", + toggle_title: "啟用模組", + toggle_description: "專案成員將能夠建立和編輯模組。", + }, + views: { + title: "檢視", + short_title: "檢視", + description: "儲存自訂排序、篩選器和顯示選項,或與團隊共享。", + toggle_title: "啟用檢視", + toggle_description: "專案成員將能夠建立和編輯檢視。", + }, + pages: { + title: "頁面", + short_title: "頁面", + description: "建立和編輯自由格式的內容:筆記、文件、任何內容。", + toggle_title: "啟用頁面", + toggle_description: "專案成員將能夠建立和編輯頁面。", + }, + intake: { + title: "接收", + short_title: "接收", + description: "讓非成員分享錯誤、回饋和建議;而不會中斷您的工作流程。", + toggle_title: "啟用接收", + toggle_description: "允許專案成員在應用程式中建立接收請求。", + }, + }, }, project_cycles: { add_cycle: "新增週期", diff --git a/packages/tailwind-config/animations.css b/packages/tailwind-config/animations.css index 3c846184c2..04835346c9 100644 --- a/packages/tailwind-config/animations.css +++ b/packages/tailwind-config/animations.css @@ -74,4 +74,15 @@ opacity: 0; } } + + /* fade in */ + --animate-fade-in: fadeIn 0.25s ease-out forwards; + @keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 9d3103c83b..7b0df5b6e8 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -36,6 +36,7 @@ export * from "./reaction"; export * from "./intake"; export * from "./rich-filters"; export * from "./search"; +export * from "./settings"; export * from "./state"; export * from "./stickies"; export * from "./timezone"; diff --git a/packages/types/src/settings.ts b/packages/types/src/settings.ts new file mode 100644 index 0000000000..b7c954a57d --- /dev/null +++ b/packages/types/src/settings.ts @@ -0,0 +1,34 @@ +// local imports +import type { EUserProjectRoles } from "."; +import type { EUserWorkspaceRoles } from "./workspace"; + +export type TProfileSettingsTabs = "general" | "preferences" | "activity" | "notifications" | "security" | "api-tokens"; + +export type TWorkspaceSettingsTabs = "general" | "members" | "billing-and-plans" | "export" | "webhooks"; +export type TWorkspaceSettingsItem = { + key: TWorkspaceSettingsTabs; + i18n_label: string; + href: string; + access: EUserWorkspaceRoles[]; + highlight: (pathname: string, baseUrl: string) => boolean; +}; + +export type TProjectSettingsTabs = + | "general" + | "members" + | "features_cycles" + | "features_modules" + | "features_views" + | "features_pages" + | "features_intake" + | "states" + | "labels" + | "estimates" + | "automations"; +export type TProjectSettingsItem = { + key: TProjectSettingsTabs; + i18n_label: string; + href: string; + access: EUserProjectRoles[]; + highlight: (pathname: string, baseUrl: string) => boolean; +}; diff --git a/packages/ui/src/tables/table.tsx b/packages/ui/src/tables/table.tsx index 956af8435e..1f798f32ba 100644 --- a/packages/ui/src/tables/table.tsx +++ b/packages/ui/src/tables/table.tsx @@ -20,8 +20,8 @@ export function Table(props: TTableData) { return ( - - + + {columns.map((column) => ( - + {data.map((item) => ( {columns.map((column) => (
{(column?.thRender && column?.thRender()) || column.content} @@ -29,11 +29,11 @@ export function Table(props: TTableData) { ))}