[WEB-5798] refactor: web and admin auth related components and update admin designs (#8431)

* refactor: web and admin auth related components and update admin designs.

* fix: format
This commit is contained in:
Prateek Shourya 2025-12-24 16:31:52 +05:30 committed by GitHub
parent 777200db7b
commit 0c795e95ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
80 changed files with 1087 additions and 950 deletions

View file

@ -114,13 +114,13 @@ export function InstanceAIForm(props: IInstanceAIForm) {
</div> </div>
</div> </div>
<div className="flex flex-col gap-2 items-start"> <div className="flex flex-col gap-4 items-start">
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}> <Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save changes"} {isSubmitting ? "Saving" : "Save changes"}
</Button> </Button>
<div className="relative inline-flex items-center gap-2 rounded-sm border border-accent-strong/20 bg-accent-primary/10 px-4 py-2 text-11 text-accent-secondary"> <div className="relative inline-flex items-center gap-1.5 rounded-sm border border-accent-subtle bg-accent-subtle px-4 py-2 text-caption-sm-regular text-accent-secondary ">
<Lightbulb height="14" width="14" /> <Lightbulb className="size-4" />
<div> <div>
If you have a preferred AI models vendor, please get in{" "} If you have a preferred AI models vendor, please get in{" "}
<a className="underline font-medium" href="https://plane.so/contact"> <a className="underline font-medium" href="https://plane.so/contact">

View file

@ -1,10 +1,13 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks // hooks
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
// components // types
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
// local
import { InstanceAIForm } from "./form"; import { InstanceAIForm } from "./form";
const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentProps) { const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentProps) {
@ -14,15 +17,12 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return ( return (
<> <PageWrapper
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col"> header={{
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0"> title: "AI features for all your workspaces",
<div className="text-18 font-medium text-primary">AI features for all your workspaces</div> description: "Configure your AI API credentials so Plane AI features are turned on for all your workspaces.",
<div className="text-13 font-regular text-tertiary"> }}
Configure your AI API credentials so Plane AI features are turned on for all your workspaces. >
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? ( {formattedConfig ? (
<InstanceAIForm config={formattedConfig} /> <InstanceAIForm config={formattedConfig} />
) : ( ) : (
@ -35,9 +35,7 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
<Loader.Item height="50px" width="20%" /> <Loader.Item height="50px" width="20%" />
</Loader> </Loader>
)} )}
</div> </PageWrapper>
</div>
</>
); );
}); });

View file

@ -196,7 +196,7 @@ export function InstanceGiteaConfigForm(props: Props) {
loading={isSubmitting} loading={isSubmitting}
disabled={!isDirty} disabled={!isDirty}
> >
{isSubmitting ? "Saving..." : "Save changes"} {isSubmitting ? "Saving" : "Save changes"}
</Button> </Button>
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}> <Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
Go back Go back
@ -205,7 +205,7 @@ export function InstanceGiteaConfigForm(props: Props) {
</div> </div>
</div> </div>
<div className="col-span-2 md:col-span-1"> <div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-layer-1/60 rounded-lg"> <div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-layer-1 rounded-lg">
<div className="pt-2 text-18 font-medium">Plane-provided details for Gitea</div> <div className="pt-2 text-18 font-medium">Plane-provided details for Gitea</div>
{GITEA_SERVICE_FIELD.map((field) => ( {GITEA_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} /> <CopyField key={field.key} label={field.label} url={field.url} description={field.description} />

View file

@ -4,13 +4,16 @@ import useSWR from "swr";
// plane internal packages // plane internal packages
import { setPromiseToast } from "@plane/propel/toast"; import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui"; import { Loader, ToggleSwitch } from "@plane/ui";
// components // assets
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url"; import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
// components
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card"; import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks // hooks
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
//local components // types
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
// local
import { InstanceGiteaConfigForm } from "./form"; import { InstanceGiteaConfigForm } from "./form";
const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthenticationPage() { const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthenticationPage() {
@ -32,7 +35,7 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
const updateConfigPromise = updateInstanceConfigurations(payload); const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, { setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration...", loading: "Saving Configuration",
success: { success: {
title: "Configuration saved", title: "Configuration saved",
message: () => `Gitea authentication is now ${value === "1" ? "active" : "disabled"}.`, message: () => `Gitea authentication is now ${value === "1" ? "active" : "disabled"}.`,
@ -56,9 +59,8 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
const isGiteaEnabled = enableGiteaConfig === "1"; const isGiteaEnabled = enableGiteaConfig === "1";
return ( return (
<> <PageWrapper
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col"> customHeader={
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard <AuthenticationMethodCard
name="Gitea" name="Gitea"
description="Allow members to login or sign up to plane with their Gitea accounts." description="Allow members to login or sign up to plane with their Gitea accounts."
@ -76,8 +78,8 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
disabled={isSubmitting || !formattedConfig} disabled={isSubmitting || !formattedConfig}
withBorder={false} withBorder={false}
/> />
</div> }
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4"> >
{formattedConfig ? ( {formattedConfig ? (
<InstanceGiteaConfigForm config={formattedConfig} /> <InstanceGiteaConfigForm config={formattedConfig} />
) : ( ) : (
@ -89,9 +91,7 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
<Loader.Item height="50px" width="50%" /> <Loader.Item height="50px" width="50%" />
</Loader> </Loader>
)} )}
</div> </PageWrapper>
</div>
</>
); );
}); });
export const meta: Route.MetaFunction = () => [{ title: "Gitea Authentication - God Mode" }]; export const meta: Route.MetaFunction = () => [{ title: "Gitea Authentication - God Mode" }];

View file

@ -217,7 +217,7 @@ export function InstanceGithubConfigForm(props: Props) {
loading={isSubmitting} loading={isSubmitting}
disabled={!isDirty} disabled={!isDirty}
> >
{isSubmitting ? "Saving..." : "Save changes"} {isSubmitting ? "Saving" : "Save changes"}
</Button> </Button>
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}> <Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
Go back Go back
@ -238,7 +238,7 @@ export function InstanceGithubConfigForm(props: Props) {
{/* web service details */} {/* web service details */}
<div className="flex flex-col rounded-lg overflow-hidden"> <div className="flex flex-col rounded-lg overflow-hidden">
<div className="px-6 py-3 bg-layer-1/60 font-medium text-11 uppercase flex items-center gap-x-3 text-secondary"> <div className="px-6 py-3 bg-layer-3 font-medium text-11 uppercase flex items-center gap-x-3 text-secondary">
<Monitor className="w-3 h-3" /> <Monitor className="w-3 h-3" />
Web Web
</div> </div>

View file

@ -6,15 +6,17 @@ import useSWR from "swr";
import { setPromiseToast } from "@plane/propel/toast"; import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui"; import { Loader, ToggleSwitch } from "@plane/ui";
import { resolveGeneralTheme } from "@plane/utils"; import { resolveGeneralTheme } from "@plane/utils";
// components // assets
import githubLightModeImage from "@/app/assets/logos/github-black.png?url"; import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
import githubDarkModeImage from "@/app/assets/logos/github-white.png?url"; import githubDarkModeImage from "@/app/assets/logos/github-white.png?url";
// components
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card"; import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks // hooks
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
// icons // types
// local components
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
// local
import { InstanceGithubConfigForm } from "./form"; import { InstanceGithubConfigForm } from "./form";
const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthenticationPage( const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthenticationPage(
@ -41,7 +43,7 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
const updateConfigPromise = updateInstanceConfigurations(payload); const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, { setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration...", loading: "Saving Configuration",
success: { success: {
title: "Configuration saved", title: "Configuration saved",
message: () => `GitHub authentication is now ${value === "1" ? "active" : "disabled"}.`, message: () => `GitHub authentication is now ${value === "1" ? "active" : "disabled"}.`,
@ -65,9 +67,8 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
const isGithubEnabled = enableGithubConfig === "1"; const isGithubEnabled = enableGithubConfig === "1";
return ( return (
<> <PageWrapper
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col"> customHeader={
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard <AuthenticationMethodCard
name="GitHub" name="GitHub"
description="Allow members to login or sign up to plane with their GitHub accounts." description="Allow members to login or sign up to plane with their GitHub accounts."
@ -92,8 +93,8 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
disabled={isSubmitting || !formattedConfig} disabled={isSubmitting || !formattedConfig}
withBorder={false} withBorder={false}
/> />
</div> }
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4"> >
{formattedConfig ? ( {formattedConfig ? (
<InstanceGithubConfigForm config={formattedConfig} /> <InstanceGithubConfigForm config={formattedConfig} />
) : ( ) : (
@ -105,9 +106,7 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
<Loader.Item height="50px" width="50%" /> <Loader.Item height="50px" width="50%" />
</Loader> </Loader>
)} )}
</div> </PageWrapper>
</div>
</>
); );
}); });

View file

@ -200,7 +200,7 @@ export function InstanceGitlabConfigForm(props: Props) {
loading={isSubmitting} loading={isSubmitting}
disabled={!isDirty} disabled={!isDirty}
> >
{isSubmitting ? "Saving..." : "Save changes"} {isSubmitting ? "Saving" : "Save changes"}
</Button> </Button>
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}> <Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
Go back Go back
@ -209,7 +209,7 @@ export function InstanceGitlabConfigForm(props: Props) {
</div> </div>
</div> </div>
<div className="col-span-2 md:col-span-1"> <div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-layer-1/60 rounded-lg"> <div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-layer-3 rounded-lg">
<div className="pt-2 text-18 font-medium">Plane-provided details for GitLab</div> <div className="pt-2 text-18 font-medium">Plane-provided details for GitLab</div>
{GITLAB_SERVICE_FIELD.map((field) => ( {GITLAB_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} /> <CopyField key={field.key} label={field.label} url={field.url} description={field.description} />

View file

@ -3,14 +3,16 @@ import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
import { setPromiseToast } from "@plane/propel/toast"; import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui"; import { Loader, ToggleSwitch } from "@plane/ui";
// components // assets
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url"; import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
// components
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card"; import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks // hooks
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
// icons // types
// local components
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
// local
import { InstanceGitlabConfigForm } from "./form"; import { InstanceGitlabConfigForm } from "./form";
const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthenticationPage( const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthenticationPage(
@ -35,7 +37,7 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
const updateConfigPromise = updateInstanceConfigurations(payload); const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, { setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration...", loading: "Saving Configuration",
success: { success: {
title: "Configuration saved", title: "Configuration saved",
message: () => `GitLab authentication is now ${value === "1" ? "active" : "disabled"}.`, message: () => `GitLab authentication is now ${value === "1" ? "active" : "disabled"}.`,
@ -56,9 +58,8 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
}); });
}; };
return ( return (
<> <PageWrapper
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col"> customHeader={
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard <AuthenticationMethodCard
name="GitLab" name="GitLab"
description="Allow members to login or sign up to plane with their GitLab accounts." description="Allow members to login or sign up to plane with their GitLab accounts."
@ -80,8 +81,8 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
disabled={isSubmitting || !formattedConfig} disabled={isSubmitting || !formattedConfig}
withBorder={false} withBorder={false}
/> />
</div> }
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4"> >
{formattedConfig ? ( {formattedConfig ? (
<InstanceGitlabConfigForm config={formattedConfig} /> <InstanceGitlabConfigForm config={formattedConfig} />
) : ( ) : (
@ -93,9 +94,7 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
<Loader.Item height="50px" width="50%" /> <Loader.Item height="50px" width="50%" />
</Loader> </Loader>
)} )}
</div> </PageWrapper>
</div>
</>
); );
}); });

View file

@ -205,7 +205,7 @@ export function InstanceGoogleConfigForm(props: Props) {
loading={isSubmitting} loading={isSubmitting}
disabled={!isDirty} disabled={!isDirty}
> >
{isSubmitting ? "Saving..." : "Save changes"} {isSubmitting ? "Saving" : "Save changes"}
</Button> </Button>
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}> <Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
Go back Go back
@ -226,7 +226,7 @@ export function InstanceGoogleConfigForm(props: Props) {
{/* web service details */} {/* web service details */}
<div className="flex flex-col rounded-lg overflow-hidden"> <div className="flex flex-col rounded-lg overflow-hidden">
<div className="px-6 py-3 bg-layer-1/60 font-medium text-11 uppercase flex items-center gap-x-3 text-secondary"> <div className="px-6 py-3 bg-layer-3 font-medium text-11 uppercase flex items-center gap-x-3 text-secondary">
<Monitor className="w-3 h-3" /> <Monitor className="w-3 h-3" />
Web Web
</div> </div>

View file

@ -3,14 +3,16 @@ import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
import { setPromiseToast } from "@plane/propel/toast"; import { setPromiseToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui"; import { Loader, ToggleSwitch } from "@plane/ui";
// components // assets
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url"; import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
// components
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card"; import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks // hooks
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
// icons // types
// local components
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
// local
import { InstanceGoogleConfigForm } from "./form"; import { InstanceGoogleConfigForm } from "./form";
const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthenticationPage( const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthenticationPage(
@ -35,7 +37,7 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
const updateConfigPromise = updateInstanceConfigurations(payload); const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, { setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration...", loading: "Saving Configuration",
success: { success: {
title: "Configuration saved", title: "Configuration saved",
message: () => `Google authentication is now ${value === "1" ? "active" : "disabled"}.`, message: () => `Google authentication is now ${value === "1" ? "active" : "disabled"}.`,
@ -56,9 +58,8 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
}); });
}; };
return ( return (
<> <PageWrapper
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col"> customHeader={
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard <AuthenticationMethodCard
name="Google" name="Google"
description="Allow members to login or sign up to plane with their Google description="Allow members to login or sign up to plane with their Google
@ -81,8 +82,8 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
disabled={isSubmitting || !formattedConfig} disabled={isSubmitting || !formattedConfig}
withBorder={false} withBorder={false}
/> />
</div> }
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4"> >
{formattedConfig ? ( {formattedConfig ? (
<InstanceGoogleConfigForm config={formattedConfig} /> <InstanceGoogleConfigForm config={formattedConfig} />
) : ( ) : (
@ -94,9 +95,7 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
<Loader.Item height="50px" width="50%" /> <Loader.Item height="50px" width="50%" />
</Loader> </Loader>
)} )}
</div> </PageWrapper>
</div>
</>
); );
}); });

View file

@ -1,27 +1,33 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useTheme } from "next-themes";
import useSWR from "swr"; import useSWR from "swr";
// plane internal packages // plane internal packages
import { setPromiseToast } from "@plane/propel/toast"; import { setPromiseToast } from "@plane/propel/toast";
import type { TInstanceConfigurationKeys } from "@plane/types"; import type { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, ToggleSwitch } from "@plane/ui"; import { Loader, ToggleSwitch } from "@plane/ui";
import { cn } from "@plane/utils"; import { cn, resolveGeneralTheme } from "@plane/utils";
// components
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks // hooks
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { useAuthenticationModes } from "@/hooks/oauth";
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
// plane admin components // types
import { AuthenticationModes } from "@/plane-admin/components/authentication";
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(_props: Route.ComponentProps) { const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(_props: Route.ComponentProps) {
// theme
const { resolvedTheme: resolvedThemeAdmin } = useTheme();
// store // store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance(); const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
// state // state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// derived values // derived values
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? ""; const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
const resolvedTheme = resolveGeneralTheme(resolvedThemeAdmin);
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => { const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
setIsSubmitting(true); setIsSubmitting(true);
@ -54,16 +60,14 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
}); });
}; };
const authenticationModes = useAuthenticationModes({ disabled: isSubmitting, updateConfig, resolvedTheme });
return ( return (
<> <PageWrapper
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col"> header={{
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0"> title: "Manage authentication modes for your instance",
<div className="text-18 font-medium text-primary">Manage authentication modes for your instance</div> description: "Configure authentication modes for your team and restrict sign-ups to be invite only.",
<div className="text-13 font-regular text-tertiary"> }}
Configure authentication modes for your team and restrict sign-ups to be invite only. >
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? ( {formattedConfig ? (
<div className="space-y-3"> <div className="space-y-3">
<div className={cn("w-full flex items-center gap-14 rounded-sm")}> <div className={cn("w-full flex items-center gap-14 rounded-sm")}>
@ -92,8 +96,18 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
</div> </div>
</div> </div>
</div> </div>
<div className="text-16 font-medium pt-6">Available authentication modes</div> <div className="text-lg font-medium pt-6">Available authentication modes</div>
<AuthenticationModes disabled={isSubmitting} updateConfig={updateConfig} /> {authenticationModes.map((method) => (
<AuthenticationMethodCard
key={method.key}
name={method.name}
description={method.description}
icon={method.icon}
config={method.config}
disabled={isSubmitting}
unavailable={method.unavailable}
/>
))}
</div> </div>
) : ( ) : (
<Loader className="space-y-10"> <Loader className="space-y-10">
@ -104,9 +118,7 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
<Loader.Item height="50px" width="20%" /> <Loader.Item height="50px" width="20%" />
</Loader> </Loader>
)} )}
</div> </PageWrapper>
</div>
</>
); );
}); });

View file

@ -209,7 +209,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
loading={isSubmitting} loading={isSubmitting}
disabled={!isValid || !isDirty} disabled={!isValid || !isDirty}
> >
{isSubmitting ? "Saving..." : "Save changes"} {isSubmitting ? "Saving" : "Save changes"}
</Button> </Button>
<Button <Button
variant="secondary" variant="secondary"

View file

@ -3,10 +3,13 @@ import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Loader, ToggleSwitch } from "@plane/ui"; import { Loader, ToggleSwitch } from "@plane/ui";
// components
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks // hooks
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
// components // types
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
// local
import { InstanceEmailForm } from "./email-config-form"; import { InstanceEmailForm } from "./email-config-form";
const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.ComponentProps) { const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.ComponentProps) {
@ -49,29 +52,29 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
}, [formattedConfig]); }, [formattedConfig]);
return ( return (
<PageWrapper
header={{
title: "Secure emails from your own instance",
description: (
<> <>
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="flex items-center justify-between gap-4 border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
<div className="py-4 space-y-1 flex-shrink-0">
<div className="text-18 font-medium text-primary">Secure emails from your own instance</div>
<div className="text-13 font-regular text-tertiary">
Plane can send useful emails to you and your users from your own instance without talking to the Internet. Plane can send useful emails to you and your users from your own instance without talking to the Internet.
<div className="text-13 font-regular text-tertiary"> <div className="text-13 font-regular text-tertiary">
Set it up below and please test your settings before you save them.&nbsp; Set it up below and please test your settings before you save them.&nbsp;
<span className="text-danger">Misconfigs can lead to email bounces and errors.</span> <span className="text-danger">Misconfigs can lead to email bounces and errors.</span>
</div> </div>
</div> </>
</div> ),
{isLoading ? ( actions: isLoading ? (
<Loader> <Loader>
<Loader.Item width="24px" height="16px" className="rounded-full" /> <Loader.Item width="24px" height="16px" className="rounded-full" />
</Loader> </Loader>
) : ( ) : (
<ToggleSwitch value={isSMTPEnabled} onChange={handleToggle} size="sm" disabled={isSubmitting} /> <ToggleSwitch value={isSMTPEnabled} onChange={handleToggle} size="sm" disabled={isSubmitting} />
)} ),
</div> }}
>
{isSMTPEnabled && !isLoading && ( {isSMTPEnabled && !isLoading && (
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4"> <>
{formattedConfig ? ( {formattedConfig ? (
<InstanceEmailForm config={formattedConfig} /> <InstanceEmailForm config={formattedConfig} />
) : ( ) : (
@ -83,10 +86,9 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
<Loader.Item height="50px" width="20%" /> <Loader.Item height="50px" width="20%" />
</Loader> </Loader>
)} )}
</div>
)}
</div>
</> </>
)}
</PageWrapper>
); );
}); });

View file

@ -121,7 +121,7 @@ export function SendTestEmailModal(props: Props) {
</Button> </Button>
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && ( {sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
<Button variant="primary" size="lg" loading={isLoading} onClick={handleSubmit} tabIndex={3}> <Button variant="primary" size="lg" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
{isLoading ? "Sending email..." : "Send email"} {isLoading ? "Sending email" : "Send email"}
</Button> </Button>
)} )}
</div> </div>

View file

@ -1,17 +1,17 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { Telescope } from "lucide-react"; import { Telescope } from "lucide-react";
// types // plane imports
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IInstance, IInstanceAdmin } from "@plane/types"; import type { IInstance, IInstanceAdmin } from "@plane/types";
// ui
import { Input, ToggleSwitch } from "@plane/ui"; import { Input, ToggleSwitch } from "@plane/ui";
// components // components
import { ControllerInput } from "@/components/common/controller-input"; import { ControllerInput } from "@/components/common/controller-input";
import { useInstance } from "@/hooks/store";
import { IntercomConfig } from "./intercom";
// hooks // hooks
import { useInstance } from "@/hooks/store";
// components
import { IntercomConfig } from "./intercom";
export interface IGeneralConfigurationForm { export interface IGeneralConfigurationForm {
instance: IInstance; instance: IInstance;
@ -27,8 +27,8 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
const { const {
handleSubmit, handleSubmit,
control, control,
watch,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
watch,
} = useForm<Partial<IInstance>>({ } = useForm<Partial<IInstance>>({
defaultValues: { defaultValues: {
instance_name: instance?.instance_name, instance_name: instance?.instance_name,
@ -105,14 +105,14 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-6">
<div className="text-16 font-medium text-primary">Chat + telemetry</div> <div className="text-16 font-medium text-primary pb-1.5 border-b border-subtle">Chat + telemetry</div>
<IntercomConfig isTelemetryEnabled={watch("is_telemetry_enabled") ?? false} /> <IntercomConfig isTelemetryEnabled={watch("is_telemetry_enabled") ?? false} />
<div className="flex items-center gap-14 px-4 py-3 border border-subtle rounded-sm"> <div className="flex items-center gap-14">
<div className="grow flex items-center gap-4"> <div className="grow flex items-center gap-4">
<div className="shrink-0"> <div className="shrink-0">
<div className="flex items-center justify-center w-10 h-10 bg-layer-1 rounded-full"> <div className="flex items-center justify-center size-11 bg-layer-1 rounded-lg">
<Telescope className="w-6 h-6 text-tertiary/80 p-0.5" /> <Telescope className="size-5 text-tertiary" />
</div> </div>
</div> </div>
<div className="grow"> <div className="grow">
@ -144,8 +144,15 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
</div> </div>
<div> <div>
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}> <Button
{isSubmitting ? "Saving..." : "Save changes"} variant="primary"
size="lg"
onClick={() => {
void handleSubmit(onSubmit)();
}}
loading={isSubmitting}
>
{isSubmitting ? "Saving" : "Save changes"}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -44,16 +44,16 @@ export const IntercomConfig = observer(function IntercomConfig(props: TIntercomC
}; };
const enableIntercomConfig = () => { const enableIntercomConfig = () => {
submitInstanceConfigurations({ IS_INTERCOM_ENABLED: isIntercomEnabled ? "0" : "1" }); void submitInstanceConfigurations({ IS_INTERCOM_ENABLED: isIntercomEnabled ? "0" : "1" });
}; };
return ( return (
<> <>
<div className="flex items-center gap-14 px-4 py-3 border border-subtle rounded-sm"> <div className="flex items-center gap-14">
<div className="grow flex items-center gap-4"> <div className="grow flex items-center gap-4">
<div className="shrink-0"> <div className="shrink-0">
<div className="flex items-center justify-center w-10 h-10 bg-layer-1 rounded-full"> <div className="flex items-center justify-center size-11 bg-layer-1 rounded-lg">
<MessageSquare className="w-6 h-6 text-tertiary/80 p-0.5" /> <MessageSquare className="size-5 text-tertiary p-0.5" />
</div> </div>
</div> </div>

View file

@ -1,30 +1,26 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// components
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks // hooks
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
// components // local imports
import type { Route } from "./+types/page";
import { GeneralConfigurationForm } from "./form"; import { GeneralConfigurationForm } from "./form";
// types
import type { Route } from "./+types/page";
function GeneralPage() { function GeneralPage() {
const { instance, instanceAdmins } = useInstance(); const { instance, instanceAdmins } = useInstance();
return ( return (
<> <PageWrapper
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col"> header={{
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0"> title: "General settings",
<div className="text-18 font-medium text-primary">General settings</div> description:
<div className="text-13 font-regular text-tertiary"> "Change the name of your instance and instance admin e-mail addresses. Enable or disable telemetry in your instance.",
Change the name of your instance and instance admin e-mail addresses. Enable or disable telemetry in your }}
instance. >
</div> {instance && instanceAdmins && <GeneralConfigurationForm instance={instance} instanceAdmins={instanceAdmins} />}
</div> </PageWrapper>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{instance && instanceAdmins && (
<GeneralConfigurationForm instance={instance} instanceAdmins={instanceAdmins} />
)}
</div>
</div>
</>
); );
} }

View file

@ -71,7 +71,7 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
<div> <div>
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}> <Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save changes"} {isSubmitting ? "Saving" : "Save changes"}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -1,10 +1,13 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components
import { PageWrapper } from "@/components/common/page-wrapper";
// hooks // hooks
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
// local // types
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
// local
import { InstanceImageConfigForm } from "./form"; import { InstanceImageConfigForm } from "./form";
const InstanceImagePage = observer(function InstanceImagePage(_props: Route.ComponentProps) { const InstanceImagePage = observer(function InstanceImagePage(_props: Route.ComponentProps) {
@ -14,15 +17,12 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return ( return (
<> <PageWrapper
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col"> header={{
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0"> title: "Third-party image libraries",
<div className="text-18 font-medium text-primary">Third-party image libraries</div> description: "Let your users search and choose images from third-party libraries",
<div className="text-13 font-regular text-tertiary"> }}
Let your users search and choose images from third-party libraries >
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? ( {formattedConfig ? (
<InstanceImageConfigForm config={formattedConfig} /> <InstanceImageConfigForm config={formattedConfig} />
) : ( ) : (
@ -31,9 +31,7 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp
<Loader.Item height="50px" width="20%" /> <Loader.Item height="50px" width="20%" />
</Loader> </Loader>
)} )}
</div> </PageWrapper>
</div>
</>
); );
}); });

View file

@ -3,13 +3,13 @@ import { observer } from "mobx-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Outlet } from "react-router"; import { Outlet } from "react-router";
// components // components
import { AdminHeader } from "@/components/common/header";
import { LogoSpinner } from "@/components/common/logo-spinner"; import { LogoSpinner } from "@/components/common/logo-spinner";
import { NewUserPopup } from "@/components/new-user-popup"; import { NewUserPopup } from "@/components/new-user-popup";
// hooks // hooks
import { useUser } from "@/hooks/store"; import { useUser } from "@/hooks/store";
// local components // local components
import type { Route } from "./+types/layout"; import type { Route } from "./+types/layout";
import { AdminHeader } from "./header";
import { AdminSidebar } from "./sidebar"; import { AdminSidebar } from "./sidebar";
function AdminLayout(_props: Route.ComponentProps) { function AdminLayout(_props: Route.ComponentProps) {

View file

@ -71,14 +71,14 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
useEffect(() => { useEffect(() => {
if (csrfToken === undefined) if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token)); void authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]); }, [csrfToken]);
return ( return (
<div className="flex max-h-header items-center gap-x-5 gap-y-2 border-b border-subtle px-4 py-3.5"> <div className="flex max-h-header items-center gap-x-5 gap-y-2 border-b border-subtle px-4 py-2.5">
<div className="h-full w-full truncate"> <div className="h-full w-full truncate">
<div <div
className={`flex flex-grow items-center gap-x-2 truncate rounded-sm py-1 ${ className={`flex flex-grow items-center gap-x-2 truncate rounded-sm ${
isSidebarCollapsed ? "justify-center" : "" isSidebarCollapsed ? "justify-center" : ""
}`} }`}
> >
@ -88,8 +88,8 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
"cursor-default": !isSidebarCollapsed, "cursor-default": !isSidebarCollapsed,
})} })}
> >
<div className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-sm bg-layer-1"> <div className="flex size-8 flex-shrink-0 items-center justify-center rounded-sm bg-layer-1">
<UserCog2 className="h-5 w-5 text-secondary" /> <UserCog2 className="size-5 text-primary" />
</div> </div>
</Menu.Button> </Menu.Button>
{isSidebarCollapsed && ( {isSidebarCollapsed && (
@ -109,7 +109,7 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
{!isSidebarCollapsed && ( {!isSidebarCollapsed && (
<div className="flex w-full gap-2"> <div className="flex w-full gap-2">
<h4 className="grow truncate text-14 font-medium text-secondary">Instance admin</h4> <h4 className="grow truncate text-body-md-medium text-primary">Instance admin</h4>
</div> </div>
)} )}
</div> </div>
@ -123,7 +123,7 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
src={getFileURL(currentUser.avatar_url)} src={getFileURL(currentUser.avatar_url)}
size={24} size={24}
shape="square" shape="square"
className="!text-14" className="!text-body-sm-medium"
/> />
</Menu.Button> </Menu.Button>

View file

@ -9,11 +9,9 @@ import { DiscordIcon, GithubIcon, PageIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip"; import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// hooks // hooks
import { useTheme } from "@/hooks/store"; import { useInstance, useTheme } from "@/hooks/store";
// assets // assets
import packageJson from "package.json";
const helpOptions = [ const helpOptions = [
{ {
name: "Documentation", name: "Documentation",
@ -36,6 +34,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
// states // states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
// store // store
const { instance } = useInstance();
const { isSidebarCollapsed, toggleSidebar } = useTheme(); const { isSidebarCollapsed, toggleSidebar } = useTheme();
// refs // refs
const helpOptionsRef = useRef<HTMLDivElement | null>(null); const helpOptionsRef = useRef<HTMLDivElement | null>(null);
@ -55,9 +54,9 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
<Tooltip tooltipContent="Redirect to Plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}> <Tooltip tooltipContent="Redirect to Plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
<a <a
href={redirectionLink} href={redirectionLink}
className={`relative px-2 py-1.5 flex items-center gap-2 font-medium rounded-sm border border-accent-strong/20 bg-accent-primary/10 text-11 text-accent-secondary whitespace-nowrap`} className={`relative px-2 py-1 flex items-center gap-1 rounded-sm bg-layer-1 text-body-xs-medium text-secondary whitespace-nowrap`}
> >
<ExternalLink size={14} /> <ExternalLink size={16} />
{!isSidebarCollapsed && "Redirect to Plane"} {!isSidebarCollapsed && "Redirect to Plane"}
</a> </a>
</Tooltip> </Tooltip>
@ -69,7 +68,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
}`} }`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)} onClick={() => setIsNeedHelpOpen((prev) => !prev)}
> >
<HelpCircle className="h-3.5 w-3.5" /> <HelpCircle className="size-4" />
</button> </button>
</Tooltip> </Tooltip>
<Tooltip tooltipContent="Toggle sidebar" position={isSidebarCollapsed ? "right" : "top"} className="ml-4"> <Tooltip tooltipContent="Toggle sidebar" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
@ -80,7 +79,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
}`} }`}
onClick={() => toggleSidebar(!isSidebarCollapsed)} onClick={() => toggleSidebar(!isSidebarCollapsed)}
> >
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isSidebarCollapsed ? "rotate-180" : ""}`} /> <MoveLeft className={`size-4 duration-300 ${isSidebarCollapsed ? "rotate-180" : ""}`} />
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
@ -108,7 +107,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
<Link href={href} key={name} target="_blank"> <Link href={href} key={name} target="_blank">
<div className="flex items-center gap-x-2 rounded-sm px-2 py-1 text-11 hover:bg-layer-1-hover"> <div className="flex items-center gap-x-2 rounded-sm px-2 py-1 text-11 hover:bg-layer-1-hover">
<div className="grid flex-shrink-0 place-items-center"> <div className="grid flex-shrink-0 place-items-center">
<Icon className="h-3.5 w-3.5 text-secondary" width={14} height={14} /> <Icon className="h-3.5 w-3.5 text-secondary" />
</div> </div>
<span className="text-11">{name}</span> <span className="text-11">{name}</span>
</div> </div>
@ -129,7 +128,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
); );
})} })}
</div> </div>
<div className="px-2 pb-1 pt-2 text-10">Version: v{packageJson.version}</div> <div className="px-2 pb-1 pt-2 text-10">Version: v{instance?.current_version}</div>
</div> </div>
</Transition> </Transition>
</div> </div>

View file

@ -1,58 +1,20 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
// plane internal packages // plane internal packages
import { WorkspaceIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip"; import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// hooks // hooks
import { useTheme } from "@/hooks/store"; import { useTheme } from "@/hooks/store";
import { useSidebarMenu } from "@/hooks/use-sidebar-menu";
const INSTANCE_ADMIN_LINKS = [
{
Icon: Cog,
name: "General",
description: "Identify your instances and get key details.",
href: `/general/`,
},
{
Icon: WorkspaceIcon,
name: "Workspaces",
description: "Manage all workspaces on this instance.",
href: `/workspace/`,
},
{
Icon: Mail,
name: "Email",
description: "Configure your SMTP controls.",
href: `/email/`,
},
{
Icon: Lock,
name: "Authentication",
description: "Configure authentication modes.",
href: `/authentication/`,
},
{
Icon: BrainCog,
name: "Artificial intelligence",
description: "Configure your OpenAI creds.",
href: `/ai/`,
},
{
Icon: Image,
name: "Images in Plane",
description: "Allow third-party image libraries.",
href: `/image/`,
},
];
export const AdminSidebarMenu = observer(function AdminSidebarMenu() { export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
// store hooks
const { isSidebarCollapsed, toggleSidebar } = useTheme();
// router // router
const pathName = usePathname(); const pathName = usePathname();
// store hooks
const { isSidebarCollapsed, toggleSidebar } = useTheme();
// derived values
const sidebarMenu = useSidebarMenu();
const handleItemClick = () => { const handleItemClick = () => {
if (window.innerWidth < 768) { if (window.innerWidth < 768) {
@ -62,40 +24,27 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
return ( return (
<div className="flex h-full w-full flex-col gap-2.5 overflow-y-scroll vertical-scrollbar scrollbar-sm px-4 py-4"> <div className="flex h-full w-full flex-col gap-2.5 overflow-y-scroll vertical-scrollbar scrollbar-sm px-4 py-4">
{INSTANCE_ADMIN_LINKS.map((item, index) => { {sidebarMenu.map((item, index) => {
const isActive = item.href === pathName || pathName.includes(item.href); const isActive = item.href === pathName || pathName?.includes(item.href);
return ( return (
<Link key={index} href={item.href} onClick={handleItemClick}> <Link key={index} href={item.href} onClick={handleItemClick}>
<div> <div>
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}> <Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}>
<div <div
className={cn( className={cn(
`group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none transition-colors`, "group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none transition-colors",
isActive {
? "bg-accent-primary/10 text-accent-primary" "text-primary !bg-layer-transparent-active": isActive,
: "text-secondary hover:bg-layer-1-hover focus:bg-layer-1-hover", "text-secondary hover:bg-layer-transparent-hover active:bg-layer-transparent-active": !isActive,
},
isSidebarCollapsed ? "justify-center" : "w-[260px]" isSidebarCollapsed ? "justify-center" : "w-[260px]"
)} )}
> >
{<item.Icon className="h-4 w-4 flex-shrink-0" />} {<item.Icon className="h-4 w-4 flex-shrink-0" />}
{!isSidebarCollapsed && ( {!isSidebarCollapsed && (
<div className="w-full "> <div className="w-full ">
<div <div className={cn(`text-body-xs-medium transition-colors`)}>{item.name}</div>
className={cn( <div className={cn(`text-caption-sm-regular transition-colors`)}>{item.description}</div>
`text-13 font-medium transition-colors`,
isActive ? "text-accent-primary" : "text-secondary"
)}
>
{item.name}
</div>
<div
className={cn(
`text-10 transition-colors`,
isActive ? "text-accent-secondary" : "text-placeholder"
)}
>
{item.description}
</div>
</div> </div>
)} )}
</div> </div>

View file

@ -1,21 +1,21 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// components // components
import { PageWrapper } from "@/components/common/page-wrapper";
// types
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
// local
import { WorkspaceCreateForm } from "./form"; import { WorkspaceCreateForm } from "./form";
const WorkspaceCreatePage = observer(function WorkspaceCreatePage(_props: Route.ComponentProps) { const WorkspaceCreatePage = observer(function WorkspaceCreatePage(_props: Route.ComponentProps) {
return ( return (
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col"> <PageWrapper
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0"> header={{
<div className="text-18 font-medium text-primary">Create a new workspace on this instance.</div> title: "Create a new workspace on this instance.",
<div className="text-13 font-regular text-tertiary"> description: "You will need to invite users from Workspace Settings after you create this workspace.",
You will need to invite users from Workspace Settings after you create this workspace. }}
</div> >
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<WorkspaceCreateForm /> <WorkspaceCreateForm />
</div> </PageWrapper>
</div>
); );
}); });

View file

@ -8,12 +8,13 @@ import { Button, getButtonStyling } from "@plane/propel/button";
import { setPromiseToast } from "@plane/propel/toast"; import { setPromiseToast } from "@plane/propel/toast";
import type { TInstanceConfigurationKeys } from "@plane/types"; import type { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, ToggleSwitch } from "@plane/ui"; import { Loader, ToggleSwitch } from "@plane/ui";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// components // components
import { PageWrapper } from "@/components/common/page-wrapper";
import { WorkspaceListItem } from "@/components/workspace/list-item"; import { WorkspaceListItem } from "@/components/workspace/list-item";
// hooks // hooks
import { useInstance, useWorkspace } from "@/hooks/store"; import { useInstance, useWorkspace } from "@/hooks/store";
// types
import type { Route } from "./+types/page"; import type { Route } from "./+types/page";
const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props: Route.ComponentProps) { const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props: Route.ComponentProps) {
@ -68,14 +69,12 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
}; };
return ( return (
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col"> <PageWrapper
<div className="flex items-center justify-between gap-4 border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0"> header={{
<div className="flex flex-col gap-1"> title: "Workspaces on this instance",
<div className="text-18 font-medium text-primary">Workspaces on this instance</div> description: "See all workspaces and control who can create them.",
<div className="text-13 font-regular text-tertiary">See all workspaces and control who can create them.</div> }}
</div> >
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<div className="space-y-3"> <div className="space-y-3">
{formattedConfig ? ( {formattedConfig ? (
<div className={cn("w-full flex items-center gap-14 rounded-sm")}> <div className={cn("w-full flex items-center gap-14 rounded-sm")}>
@ -83,8 +82,7 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
<div className="grow"> <div className="grow">
<div className="text-16 font-medium pb-1">Prevent anyone else from creating a workspace.</div> <div className="text-16 font-medium pb-1">Prevent anyone else from creating a workspace.</div>
<div className={cn("font-regular leading-5 text-tertiary text-11")}> <div className={cn("font-regular leading-5 text-tertiary text-11")}>
Toggling this on will let only you create workspaces. You will have to invite users to new Toggling this on will let only you create workspaces. You will have to invite users to new workspaces.
workspaces.
</div> </div>
</div> </div>
</div> </div>
@ -126,7 +124,7 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Link href="/workspace/create" className={getButtonStyling("primary", "lg")}> <Link href="/workspace/create" className={getButtonStyling("primary", "base")}>
Create workspace Create workspace
</Link> </Link>
</div> </div>
@ -159,8 +157,7 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
</Loader> </Loader>
)} )}
</div> </div>
</div> </PageWrapper>
</div>
); );
}); });

View file

@ -20,7 +20,7 @@ export function AuthBanner(props: TAuthBanner) {
</div> </div>
<div className="w-full text-13 font-medium text-accent-primary">{bannerData?.message}</div> <div className="w-full text-13 font-medium text-accent-primary">{bannerData?.message}</div>
<div <div
className="relative ml-auto w-6 h-6 rounded-xs flex justify-center items-center transition-all cursor-pointer hover:bg-accent-primary/20 text-accent-primary/80" className="relative ml-auto w-6 h-6 rounded-xs flex justify-center items-center transition-all cursor-pointer hover:bg-accent-primary/20 text-accent-primary"
onClick={() => handleBannerData && handleBannerData(undefined)} onClick={() => handleBannerData && handleBannerData(undefined)}
> >
<CloseIcon className="w-4 h-4 flex-shrink-0" /> <CloseIcon className="w-4 h-4 flex-shrink-0" />

View file

@ -117,14 +117,14 @@ export const getBaseAuthenticationModes: (props: TGetBaseAuthenticationModeProps
name: "Unique codes", name: "Unique codes",
description: description:
"Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.", "Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
icon: <Mails className="h-6 w-6 p-0.5 text-tertiary/80" />, icon: <Mails className="h-6 w-6 p-0.5 text-tertiary" />,
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />, config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
}, },
{ {
key: "passwords-login", key: "passwords-login",
name: "Passwords", name: "Passwords",
description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.", description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.",
icon: <KeyRound className="h-6 w-6 p-0.5 text-tertiary/80" />, icon: <KeyRound className="h-6 w-6 p-0.5 text-tertiary" />,
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />, config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
}, },
{ {

View file

@ -1,15 +1,15 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import * as Sentry from "@sentry/react-router";
import { Links, Meta, Outlet, Scripts } from "react-router"; import { Links, Meta, Outlet, Scripts } from "react-router";
import type { LinksFunction } from "react-router"; import type { LinksFunction } from "react-router";
import * as Sentry from "@sentry/react-router";
import appleTouchIcon from "@/app/assets/favicon/apple-touch-icon.png?url"; import appleTouchIcon from "@/app/assets/favicon/apple-touch-icon.png?url";
import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url"; import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url";
import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url"; import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url";
import faviconIco from "@/app/assets/favicon/favicon.ico?url"; import faviconIco from "@/app/assets/favicon/favicon.ico?url";
import { LogoSpinner } from "@/components/common/logo-spinner"; import { LogoSpinner } from "@/components/common/logo-spinner";
import globalStyles from "@/styles/globals.css?url"; import globalStyles from "@/styles/globals.css?url";
import { AppProviders } from "@/providers";
import type { Route } from "./+types/root"; import type { Route } from "./+types/root";
import { AppProviders } from "./providers";
// fonts // fonts
import "@fontsource-variable/inter"; import "@fontsource-variable/inter";
import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url"; import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url";

View file

@ -1 +0,0 @@
export * from "./authentication-modes";

View file

@ -1 +0,0 @@
export * from "./upgrade-button";

View file

@ -1,20 +0,0 @@
import React from "react";
// icons
import { SquareArrowOutUpRight } from "lucide-react";
// plane internal packages
import { getButtonStyling } from "@plane/propel/button";
import { cn } from "@plane/utils";
export function UpgradeButton() {
return (
<a
href="https://plane.so/pricing?mode=self-hosted"
target="_blank"
className={cn(getButtonStyling("primary", "base"))}
rel="noreferrer"
>
Upgrade
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
</a>
);
}

View file

@ -16,7 +16,7 @@ export function AuthenticationMethodCard(props: Props) {
return ( return (
<div <div
className={cn("w-full flex items-center gap-14 rounded-sm", { className={cn("w-full flex items-center gap-14 rounded-lg bg-layer-2", {
"px-4 py-3 border border-subtle": withBorder, "px-4 py-3 border border-subtle": withBorder,
})} })}
> >

View file

@ -44,7 +44,7 @@ export const GiteaConfiguration = observer(function GiteaConfiguration(props: Pr
</div> </div>
) : ( ) : (
<Link href="/authentication/gitea" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}> <Link href="/authentication/gitea" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
<Settings2 className="h-4 w-4 p-0.5 text-tertiary/80" /> <Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
Configure Configure
</Link> </Link>
)} )}

View file

@ -1,4 +1,3 @@
import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Link from "next/link"; import Link from "next/link";
// icons // icons
@ -43,7 +42,7 @@ export const GithubConfiguration = observer(function GithubConfiguration(props:
</div> </div>
) : ( ) : (
<Link href="/authentication/github" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}> <Link href="/authentication/github" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
<Settings2 className="h-4 w-4 p-0.5 text-tertiary/80" /> <Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
Configure Configure
</Link> </Link>
)} )}

View file

@ -42,7 +42,7 @@ export const GitlabConfiguration = observer(function GitlabConfiguration(props:
</div> </div>
) : ( ) : (
<Link href="/authentication/gitlab" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}> <Link href="/authentication/gitlab" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
<Settings2 className="h-4 w-4 p-0.5 text-tertiary/80" /> <Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
Configure Configure
</Link> </Link>
)} )}

View file

@ -42,7 +42,7 @@ export const GoogleConfiguration = observer(function GoogleConfiguration(props:
</div> </div>
) : ( ) : (
<Link href="/authentication/google" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}> <Link href="/authentication/google" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
<Settings2 className="h-4 w-4 p-0.5 text-tertiary/80" /> <Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
Configure Configure
</Link> </Link>
)} )}

View file

@ -0,0 +1,13 @@
export const CORE_HEADER_SEGMENT_LABELS: Record<string, string> = {
general: "General",
ai: "Artificial Intelligence",
email: "Email",
authentication: "Authentication",
image: "Image",
google: "Google",
github: "GitHub",
gitlab: "GitLab",
gitea: "Gitea",
workspace: "Workspace",
create: "Create",
};

View file

@ -0,0 +1 @@
export const EXTENDED_HEADER_SEGMENT_LABELS: Record<string, string> = {};

View file

@ -7,51 +7,30 @@ import { Breadcrumbs } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common/breadcrumb-link"; import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
// hooks // hooks
import { useTheme } from "@/hooks/store"; import { useTheme } from "@/hooks/store";
// local imports
import { CORE_HEADER_SEGMENT_LABELS } from "./core";
import { EXTENDED_HEADER_SEGMENT_LABELS } from "./extended";
export const HamburgerToggle = observer(function HamburgerToggle() { export const HamburgerToggle = observer(function HamburgerToggle() {
const { isSidebarCollapsed, toggleSidebar } = useTheme(); const { isSidebarCollapsed, toggleSidebar } = useTheme();
return ( return (
<div <button
className="w-7 h-7 rounded-sm flex justify-center items-center bg-layer-1 transition-all hover:bg-layer-1-hover cursor-pointer group md:hidden" className="size-7 rounded-sm flex justify-center items-center bg-layer-1 transition-all hover:bg-layer-1-hover cursor-pointer group md:hidden"
onClick={() => toggleSidebar(!isSidebarCollapsed)} onClick={() => toggleSidebar(!isSidebarCollapsed)}
> >
<Menu size={14} className="text-secondary group-hover:text-primary transition-all" /> <Menu size={14} className="text-secondary group-hover:text-primary transition-all" />
</div> </button>
); );
}); });
const HEADER_SEGMENT_LABELS = {
...CORE_HEADER_SEGMENT_LABELS,
...EXTENDED_HEADER_SEGMENT_LABELS,
};
export const AdminHeader = observer(function AdminHeader() { export const AdminHeader = observer(function AdminHeader() {
const pathName = usePathname(); const pathName = usePathname();
const getHeaderTitle = (pathName: string) => {
switch (pathName) {
case "general":
return "General";
case "ai":
return "Artificial Intelligence";
case "email":
return "Email";
case "authentication":
return "Authentication";
case "image":
return "Image";
case "google":
return "Google";
case "github":
return "GitHub";
case "gitlab":
return "GitLab";
case "gitea":
return "Gitea";
case "workspace":
return "Workspace";
case "create":
return "Create";
default:
return pathName.toUpperCase();
}
};
// Function to dynamically generate breadcrumb items based on pathname // Function to dynamically generate breadcrumb items based on pathname
const generateBreadcrumbItems = (pathname: string) => { const generateBreadcrumbItems = (pathname: string) => {
const pathSegments = pathname.split("/").slice(1); // removing the first empty string. const pathSegments = pathname.split("/").slice(1); // removing the first empty string.
@ -61,14 +40,14 @@ export const AdminHeader = observer(function AdminHeader() {
const breadcrumbItems = pathSegments.map((segment) => { const breadcrumbItems = pathSegments.map((segment) => {
currentUrl += "/" + segment; currentUrl += "/" + segment;
return { return {
title: getHeaderTitle(segment), title: HEADER_SEGMENT_LABELS[segment] ?? segment.toUpperCase(),
href: currentUrl, href: currentUrl,
}; };
}); });
return breadcrumbItems; return breadcrumbItems;
}; };
const breadcrumbItems = generateBreadcrumbItems(pathName); const breadcrumbItems = generateBreadcrumbItems(pathName || "");
return ( return (
<div className="relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-subtle bg-surface-1 p-4"> <div className="relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-subtle bg-surface-1 p-4">

View file

@ -0,0 +1,44 @@
import type { ReactNode } from "react";
// plane imports
import { cn } from "@plane/utils";
type TPageWrapperProps = {
children: ReactNode;
header?: {
title: string;
description: string | ReactNode;
actions?: ReactNode;
};
customHeader?: ReactNode;
size?: "lg" | "md";
};
export const PageWrapper = (props: TPageWrapperProps) => {
const { children, header, customHeader, size = "md" } = props;
return (
<div
className={cn("mx-auto w-full h-full space-y-6 py-4", {
"md:px-4 max-w-[1000px] 2xl:max-w-[1200px]": size === "md",
"px-4 lg:px-12": size === "lg",
})}
>
{customHeader ? (
<div className="border-b border-subtle mx-4 py-4 space-y-1 shrink-0">{customHeader}</div>
) : (
header && (
<div className="flex items-center justify-between gap-4 border-b border-subtle mx-4 py-4 space-y-1 shrink-0">
<div className={header.actions ? "flex flex-col gap-1" : "space-y-1"}>
<div className="text-primary text-h5-semibold">{header.title}</div>
<div className="text-secondary text-body-sm-regular">{header.description}</div>
</div>
{header.actions && <div className="shrink-0">{header.actions}</div>}
</div>
)
)}
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-sm px-4 pb-4">
{children}
</div>
</div>
);
};

View file

@ -54,13 +54,13 @@ const defaultFromData: TFormData = {
export function InstanceSetupForm() { export function InstanceSetupForm() {
// search params // search params
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const firstNameParam = searchParams.get("first_name") || undefined; const firstNameParam = searchParams?.get("first_name") || undefined;
const lastNameParam = searchParams.get("last_name") || undefined; const lastNameParam = searchParams?.get("last_name") || undefined;
const companyParam = searchParams.get("company") || undefined; const companyParam = searchParams?.get("company") || undefined;
const emailParam = searchParams.get("email") || undefined; const emailParam = searchParams?.get("email") || undefined;
const isTelemetryEnabledParam = (searchParams.get("is_telemetry_enabled") === "True" ? true : false) || true; const isTelemetryEnabledParam = (searchParams?.get("is_telemetry_enabled") === "True" ? true : false) || true;
const errorCode = searchParams.get("error_code") || undefined; const errorCode = searchParams?.get("error_code") || undefined;
const errorMessage = searchParams.get("error_message") || undefined; const errorMessage = searchParams?.get("error_message") || undefined;
// state // state
const [showPassword, setShowPassword] = useState({ const [showPassword, setShowPassword] = useState({
password: false, password: false,
@ -238,7 +238,7 @@ export function InstanceSetupForm() {
name="password" name="password"
type={showPassword.password ? "text" : "password"} type={showPassword.password ? "text" : "password"}
inputSize="md" inputSize="md"
placeholder="New password..." placeholder="New password"
value={formData.password} value={formData.password}
onChange={(e) => handleFormChange("password", e.target.value)} onChange={(e) => handleFormChange("password", e.target.value)}
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false} hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}

View file

@ -23,13 +23,13 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
key={workspaceId} key={workspaceId}
href={`${WEB_BASE_URL}/${encodeURIComponent(workspace.slug)}`} href={`${WEB_BASE_URL}/${encodeURIComponent(workspace.slug)}`}
target="_blank" target="_blank"
className="group flex items-center justify-between p-4 gap-2.5 truncate border border-subtle/70 hover:border-subtle bg-layer-1 hover:bg-layer-1-hover rounded-md" className="group flex items-center justify-between p-3 gap-2.5 truncate border border-subtle hover:border-subtle-1 bg-layer-1 hover:bg-layer-1-hover hover:shadow-raised-100 rounded-lg"
rel="noreferrer" rel="noreferrer"
> >
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<span <span
className={`relative flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 mt-1 text-11 uppercase ${ className={`relative flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 mt-1 text-11 uppercase ${
!workspace?.logo_url && "rounded-sm bg-accent-primary text-on-color" !workspace?.logo_url && "rounded-lg bg-accent-primary text-on-color"
}`} }`}
> >
{workspace?.logo_url && workspace.logo_url !== "" ? ( {workspace?.logo_url && workspace.logo_url !== "" ? (
@ -75,7 +75,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
</div> </div>
</div> </div>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<ExternalLink size={14} className="text-placeholder group-hover:text-secondary" /> <ExternalLink size={16} className="text-placeholder group-hover:text-secondary" />
</div> </div>
</a> </a>
); );

View file

@ -1,72 +1,61 @@
import { observer } from "mobx-react";
import { useTheme } from "next-themes";
import { KeyRound, Mails } from "lucide-react"; import { KeyRound, Mails } from "lucide-react";
// types // types
import type { import type {
TCoreInstanceAuthenticationModeKeys,
TGetBaseAuthenticationModeProps, TGetBaseAuthenticationModeProps,
TInstanceAuthenticationMethodKeys,
TInstanceAuthenticationModes, TInstanceAuthenticationModes,
} from "@plane/types"; } from "@plane/types";
import { resolveGeneralTheme } from "@plane/utils"; // assets
// components
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url"; import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
import githubLightModeImage from "@/app/assets/logos/github-black.png?url"; import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
import githubDarkModeImage from "@/app/assets/logos/github-white.png?url"; import githubDarkModeImage from "@/app/assets/logos/github-white.png?url";
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url"; import gitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url"; import googleLogo from "@/app/assets/logos/google-logo.svg?url";
import OIDCLogo from "@/app/assets/logos/oidc-logo.svg?url"; // components
import SAMLLogo from "@/app/assets/logos/saml-logo.svg?url";
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch"; import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch";
import { GiteaConfiguration } from "@/components/authentication/gitea-config"; import { GiteaConfiguration } from "@/components/authentication/gitea-config";
import { GithubConfiguration } from "@/components/authentication/github-config"; import { GithubConfiguration } from "@/components/authentication/github-config";
import { GitlabConfiguration } from "@/components/authentication/gitlab-config"; import { GitlabConfiguration } from "@/components/authentication/gitlab-config";
import { GoogleConfiguration } from "@/components/authentication/google-config"; import { GoogleConfiguration } from "@/components/authentication/google-config";
import { PasswordLoginConfiguration } from "@/components/authentication/password-config-switch"; import { PasswordLoginConfiguration } from "@/components/authentication/password-config-switch";
// plane admin components
import { UpgradeButton } from "@/plane-admin/components/common";
// assets
export type TAuthenticationModeProps = {
disabled: boolean;
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
// Authentication methods // Authentication methods
export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({ export const getCoreAuthenticationModesMap: (
props: TGetBaseAuthenticationModeProps
) => Record<TCoreInstanceAuthenticationModeKeys, TInstanceAuthenticationModes> = ({
disabled, disabled,
updateConfig, updateConfig,
resolvedTheme, resolvedTheme,
}) => [ }) => ({
{ "unique-codes": {
key: "unique-codes", key: "unique-codes",
name: "Unique codes", name: "Unique codes",
description: description:
"Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.", "Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
icon: <Mails className="h-6 w-6 p-0.5 text-tertiary/80" />, icon: <Mails className="h-6 w-6 p-0.5 text-tertiary" />,
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />, config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
}, },
{ "passwords-login": {
key: "passwords-login", key: "passwords-login",
name: "Passwords", name: "Passwords",
description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.", description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.",
icon: <KeyRound className="h-6 w-6 p-0.5 text-tertiary/80" />, icon: <KeyRound className="h-6 w-6 p-0.5 text-tertiary" />,
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />, config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
}, },
{ google: {
key: "google", key: "google",
name: "Google", name: "Google",
description: "Allow members to log in or sign up for Plane with their Google accounts.", description: "Allow members to log in or sign up for Plane with their Google accounts.",
icon: <img src={GoogleLogo} height={20} width={20} alt="Google Logo" />, icon: <img src={googleLogo} height={20} width={20} alt="Google Logo" />,
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />, config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
}, },
{ github: {
key: "github", key: "github",
name: "GitHub", name: "GitHub",
description: "Allow members to log in or sign up for Plane with their GitHub accounts.", description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
icon: ( icon: (
<img <img
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage} src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20} height={20}
width={20} width={20}
alt="GitHub Logo" alt="GitHub Logo"
@ -74,56 +63,18 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
), ),
config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />, config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />,
}, },
{ gitlab: {
key: "gitlab", key: "gitlab",
name: "GitLab", name: "GitLab",
description: "Allow members to log in or sign up to plane with their GitLab accounts.", description: "Allow members to log in or sign up to plane with their GitLab accounts.",
icon: <img src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />, icon: <img src={gitlabLogo} height={20} width={20} alt="GitLab Logo" />,
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />, config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
}, },
{ gitea: {
key: "gitea", key: "gitea",
name: "Gitea", name: "Gitea",
description: "Allow members to log in or sign up to plane with their Gitea accounts.", description: "Allow members to log in or sign up to plane with their Gitea accounts.",
icon: <img src={giteaLogo} height={20} width={20} alt="Gitea Logo" />, icon: <img src={giteaLogo} height={20} width={20} alt="Gitea Logo" />,
config: <GiteaConfiguration disabled={disabled} updateConfig={updateConfig} />, config: <GiteaConfiguration disabled={disabled} updateConfig={updateConfig} />,
}, },
{
key: "oidc",
name: "OIDC",
description: "Authenticate your users via the OpenID Connect protocol.",
icon: <img src={OIDCLogo} height={22} width={22} alt="OIDC Logo" />,
config: <UpgradeButton />,
unavailable: true,
},
{
key: "saml",
name: "SAML",
description: "Authenticate your users via the Security Assertion Markup Language protocol.",
icon: <img src={SAMLLogo} height={22} width={22} alt="SAML Logo" className="pl-0.5" />,
config: <UpgradeButton />,
unavailable: true,
},
];
export const AuthenticationModes = observer(function AuthenticationModes(props: TAuthenticationModeProps) {
const { disabled, updateConfig } = props;
// next-themes
const { resolvedTheme } = useTheme();
return (
<div className="flex flex-col gap-3">
{getAuthenticationModes({ disabled, updateConfig, resolvedTheme }).map((method) => (
<AuthenticationMethodCard
key={method.key}
name={method.name}
description={method.description}
icon={method.icon}
config={method.config}
disabled={disabled}
unavailable={method.unavailable}
/>
))}
</div>
);
}); });

View file

@ -0,0 +1,19 @@
import type { TInstanceAuthenticationModes } from "@plane/types";
import { getCoreAuthenticationModesMap } from "./core";
import type { TGetAuthenticationModeProps } from "./types";
export const useAuthenticationModes = (props: TGetAuthenticationModeProps): TInstanceAuthenticationModes[] => {
// derived values
const authenticationModes = getCoreAuthenticationModesMap(props);
const availableAuthenticationModes: TInstanceAuthenticationModes[] = [
authenticationModes["unique-codes"],
authenticationModes["passwords-login"],
authenticationModes["google"],
authenticationModes["github"],
authenticationModes["gitlab"],
authenticationModes["gitea"],
];
return availableAuthenticationModes;
};

View file

@ -0,0 +1,7 @@
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
export type TGetAuthenticationModeProps = {
disabled: boolean;
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
resolvedTheme: string | undefined;
};

View file

@ -1,6 +1,6 @@
import { useContext } from "react"; import { useContext } from "react";
// store // store
import { StoreContext } from "@/app/(all)/store.provider"; import { StoreContext } from "@/providers/store.provider";
import type { IInstanceStore } from "@/store/instance.store"; import type { IInstanceStore } from "@/store/instance.store";
export const useInstance = (): IInstanceStore => { export const useInstance = (): IInstanceStore => {

View file

@ -1,6 +1,6 @@
import { useContext } from "react"; import { useContext } from "react";
// store // store
import { StoreContext } from "@/app/(all)/store.provider"; import { StoreContext } from "@/providers/store.provider";
import type { IThemeStore } from "@/store/theme.store"; import type { IThemeStore } from "@/store/theme.store";
export const useTheme = (): IThemeStore => { export const useTheme = (): IThemeStore => {

View file

@ -1,6 +1,6 @@
import { useContext } from "react"; import { useContext } from "react";
// store // store
import { StoreContext } from "@/app/(all)/store.provider"; import { StoreContext } from "@/providers/store.provider";
import type { IUserStore } from "@/store/user.store"; import type { IUserStore } from "@/store/user.store";
export const useUser = (): IUserStore => { export const useUser = (): IUserStore => {

View file

@ -1,6 +1,6 @@
import { useContext } from "react"; import { useContext } from "react";
// store // store
import { StoreContext } from "@/app/(all)/store.provider"; import { StoreContext } from "@/providers/store.provider";
import type { IWorkspaceStore } from "@/store/workspace.store"; import type { IWorkspaceStore } from "@/store/workspace.store";
export const useWorkspace = (): IWorkspaceStore => { export const useWorkspace = (): IWorkspaceStore => {

View file

@ -0,0 +1,46 @@
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
// plane imports
import { WorkspaceIcon } from "@plane/propel/icons";
// types
import type { TSidebarMenuItem } from "./types";
export type TCoreSidebarMenuKey = "general" | "email" | "workspace" | "authentication" | "ai" | "image";
export const coreSidebarMenuLinks: Record<TCoreSidebarMenuKey, TSidebarMenuItem> = {
general: {
Icon: Cog,
name: "General",
description: "Identify your instances and get key details.",
href: `/general/`,
},
email: {
Icon: Mail,
name: "Email",
description: "Configure your SMTP controls.",
href: `/email/`,
},
workspace: {
Icon: WorkspaceIcon,
name: "Workspaces",
description: "Manage all workspaces on this instance.",
href: `/workspace/`,
},
authentication: {
Icon: Lock,
name: "Authentication",
description: "Configure authentication modes.",
href: `/authentication/`,
},
ai: {
Icon: BrainCog,
name: "Artificial intelligence",
description: "Configure your OpenAI creds.",
href: `/ai/`,
},
image: {
Icon: Image,
name: "Images in Plane",
description: "Allow third-party image libraries.",
href: `/image/`,
},
};

View file

@ -0,0 +1,14 @@
// local imports
import { coreSidebarMenuLinks } from "./core";
import type { TSidebarMenuItem } from "./types";
export function useSidebarMenu(): TSidebarMenuItem[] {
return [
coreSidebarMenuLinks.general,
coreSidebarMenuLinks.email,
coreSidebarMenuLinks.authentication,
coreSidebarMenuLinks.workspace,
coreSidebarMenuLinks.ai,
coreSidebarMenuLinks.image,
];
}

View file

@ -0,0 +1,8 @@
import type { LucideIcon } from "lucide-react";
export type TSidebarMenuItem = {
Icon: LucideIcon | React.ComponentType<{ className?: string }>;
name: string;
description: string;
href: string;
};

View file

@ -1,10 +1,11 @@
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from "next-themes";
import { SWRConfig } from "swr"; import { SWRConfig } from "swr";
import { AppProgressBar } from "@/lib/b-progress"; import { AppProgressBar } from "@/lib/b-progress";
import { InstanceProvider } from "./(all)/instance.provider"; // local imports
import { StoreProvider } from "./(all)/store.provider"; import { ToastWithTheme } from "./toast";
import { ToastWithTheme } from "./(all)/toast"; import { StoreProvider } from "./store.provider";
import { UserProvider } from "./(all)/user.provider"; import { InstanceProvider } from "./instance.provider";
import { UserProvider } from "./user.provider";
const DEFAULT_SWR_CONFIG = { const DEFAULT_SWR_CONFIG = {
refreshWhenHidden: false, refreshWhenHidden: false,
@ -15,7 +16,7 @@ const DEFAULT_SWR_CONFIG = {
errorRetryCount: 3, errorRetryCount: 3,
}; };
export function AppProviders({ children }: { children: React.ReactNode }) { export function CoreProviders({ children }: { children: React.ReactNode }) {
return ( return (
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem> <ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
<AppProgressBar /> <AppProgressBar />

View file

@ -0,0 +1,3 @@
export function ExtendedProviders({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}

View file

@ -0,0 +1,10 @@
import { CoreProviders } from "./core";
import { ExtendedProviders } from "./extended";
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<CoreProviders>
<ExtendedProviders>{children}</ExtendedProviders>
</CoreProviders>
);
}

View file

@ -1 +0,0 @@
export * from "ce/components/authentication/authentication-modes";

View file

@ -1 +0,0 @@
export * from "./authentication-modes";

View file

@ -1 +0,0 @@
export * from "ce/components/common";

View file

@ -1,22 +1,15 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { useTheme } from "next-themes";
// plane imports // plane imports
import { API_BASE_URL } from "@plane/constants";
import { SitesAuthService } from "@plane/services"; import { SitesAuthService } from "@plane/services";
import type { IEmailCheckData } from "@plane/types"; import type { IEmailCheckData } from "@plane/types";
import { OAuthOptions } from "@plane/ui"; import { OAuthOptions } from "@plane/ui";
// assets
import GiteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
import GithubLightLogo from "@/app/assets/logos/github-black.png?url";
import GithubDarkLogo from "@/app/assets/logos/github-dark.svg?url";
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
// helpers // helpers
import type { TAuthErrorInfo } from "@/helpers/authentication.helper"; import type { TAuthErrorInfo } from "@/helpers/authentication.helper";
import { EErrorAlertType, authErrorHandler, EAuthenticationErrorCodes } from "@/helpers/authentication.helper"; import { EErrorAlertType, authErrorHandler, EAuthenticationErrorCodes } from "@/helpers/authentication.helper";
// hooks // hooks
import { useOAuthConfig } from "@/hooks/oauth";
import { useInstance } from "@/hooks/store/use-instance"; import { useInstance } from "@/hooks/store/use-instance";
// types // types
import { EAuthModes, EAuthSteps } from "@/types/auth"; import { EAuthModes, EAuthSteps } from "@/types/auth";
@ -36,7 +29,6 @@ export const AuthRoot = observer(function AuthRoot() {
const emailParam = searchParams.get("email") || undefined; const emailParam = searchParams.get("email") || undefined;
const error_code = searchParams.get("error_code") || undefined; const error_code = searchParams.get("error_code") || undefined;
const nextPath = searchParams.get("next_path") || undefined; const nextPath = searchParams.get("next_path") || undefined;
const next_path = searchParams.get("next_path");
// states // states
const [authMode, setAuthMode] = useState<EAuthModes>(EAuthModes.SIGN_UP); const [authMode, setAuthMode] = useState<EAuthModes>(EAuthModes.SIGN_UP);
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL); const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
@ -44,7 +36,6 @@ export const AuthRoot = observer(function AuthRoot() {
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined); const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
const [isPasswordAutoset, setIsPasswordAutoset] = useState(true); const [isPasswordAutoset, setIsPasswordAutoset] = useState(true);
// hooks // hooks
const { resolvedTheme } = useTheme();
const { config } = useInstance(); const { config } = useInstance();
useEffect(() => { useEffect(() => {
@ -87,13 +78,8 @@ export const AuthRoot = observer(function AuthRoot() {
const isSMTPConfigured = config?.is_smtp_configured || false; const isSMTPConfigured = config?.is_smtp_configured || false;
const isMagicLoginEnabled = config?.is_magic_login_enabled || false; const isMagicLoginEnabled = config?.is_magic_login_enabled || false;
const isEmailPasswordEnabled = config?.is_email_password_enabled || false; const isEmailPasswordEnabled = config?.is_email_password_enabled || false;
const isOAuthEnabled = const oAuthActionText = authMode === EAuthModes.SIGN_UP ? "Sign up" : "Sign in";
(config && const { isOAuthEnabled, oAuthOptions } = useOAuthConfig(oAuthActionText);
(config?.is_google_enabled ||
config?.is_github_enabled ||
config?.is_gitlab_enabled ||
config?.is_gitea_enabled)) ||
false;
// submit handler- email verification // submit handler- email verification
const handleEmailVerification = async (data: IEmailCheckData) => { const handleEmailVerification = async (data: IEmailCheckData) => {
@ -153,54 +139,6 @@ export const AuthRoot = observer(function AuthRoot() {
}); });
}; };
const content = authMode === EAuthModes.SIGN_UP ? "Sign up" : "Sign in";
const OAuthConfig = [
{
id: "google",
text: `${content} with Google`,
icon: <img src={GoogleLogo} height={18} width={18} alt="Google Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/google/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_google_enabled,
},
{
id: "github",
text: `${content} with GitHub`,
icon: (
<img
src={resolvedTheme === "dark" ? GithubLightLogo : GithubDarkLogo}
height={18}
width={18}
alt="GitHub Logo"
/>
),
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/github/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_github_enabled,
},
{
id: "gitlab",
text: `${content} with GitLab`,
icon: <img src={GitlabLogo} height={18} width={18} alt="GitLab Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/gitlab/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_gitlab_enabled,
},
{
id: "gitea",
text: `${content} with Gitea`,
icon: <img src={GiteaLogo} height={18} width={18} alt="Gitea Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/gitea/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_gitea_enabled,
},
];
return ( return (
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10"> <div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full"> <div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
@ -208,7 +146,7 @@ export const AuthRoot = observer(function AuthRoot() {
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} /> <AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
)} )}
<AuthHeader authMode={authMode} /> <AuthHeader authMode={authMode} />
{isOAuthEnabled && <OAuthOptions options={OAuthConfig} compact={authStep === EAuthSteps.PASSWORD} />} {isOAuthEnabled && <OAuthOptions options={oAuthOptions} compact={authStep === EAuthSteps.PASSWORD} />}
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />} {authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
{authStep === EAuthSteps.UNIQUE_CODE && ( {authStep === EAuthSteps.UNIQUE_CODE && (

View file

@ -0,0 +1,82 @@
// plane imports
import { useSearchParams } from "next/navigation";
import { useTheme } from "next-themes";
import { API_BASE_URL } from "@plane/constants";
import type { TOAuthConfigs, TOAuthOption } from "@plane/types";
// assets
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
import githubLightLogo from "@/app/assets/logos/github-black.png?url";
import githubDarkLogo from "@/app/assets/logos/github-dark.svg?url";
import gitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
import googleLogo from "@/app/assets/logos/google-logo.svg?url";
// hooks
import { useInstance } from "@/hooks/store/use-instance";
export const useCoreOAuthConfig = (oauthActionText: string): TOAuthConfigs => {
//router
const searchParams = useSearchParams();
// query params
const next_path = searchParams.get("next_path");
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { config } = useInstance();
// derived values
const isOAuthEnabled =
(config &&
(config?.is_google_enabled ||
config?.is_github_enabled ||
config?.is_gitlab_enabled ||
config?.is_gitea_enabled)) ||
false;
const oAuthOptions: TOAuthOption[] = [
{
id: "google",
text: `${oauthActionText} with Google`,
icon: <img src={googleLogo} height={18} width={18} alt="Google Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/google/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_google_enabled,
},
{
id: "github",
text: `${oauthActionText} with GitHub`,
icon: (
<img
src={resolvedTheme === "dark" ? githubLightLogo : githubDarkLogo}
height={18}
width={18}
alt="GitHub Logo"
/>
),
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/github/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_github_enabled,
},
{
id: "gitlab",
text: `${oauthActionText} with GitLab`,
icon: <img src={gitlabLogo} height={18} width={18} alt="GitLab Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/gitlab/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_gitlab_enabled,
},
{
id: "gitea",
text: `${oauthActionText} with Gitea`,
icon: <img src={giteaLogo} height={18} width={18} alt="Gitea Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/gitea/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_gitea_enabled,
},
];
return {
isOAuthEnabled,
oAuthOptions,
};
};

View file

@ -0,0 +1,7 @@
// plane imports
import type { TOAuthConfigs } from "@plane/types";
export const useExtendedOAuthConfig = (_oauthActionText: string): TOAuthConfigs => ({
isOAuthEnabled: false,
oAuthOptions: [],
});

View file

@ -0,0 +1,14 @@
// plane imports
import type { TOAuthConfigs } from "@plane/types";
// local imports
import { useCoreOAuthConfig } from "./core";
import { useExtendedOAuthConfig } from "./extended";
export const useOAuthConfig = (oauthActionText: string = "Continue"): TOAuthConfigs => {
const coreOAuthConfig = useCoreOAuthConfig(oauthActionText);
const extendedOAuthConfig = useExtendedOAuthConfig(oauthActionText);
return {
isOAuthEnabled: coreOAuthConfig.isOAuthEnabled || extendedOAuthConfig.isOAuthEnabled,
oAuthOptions: [...coreOAuthConfig.oAuthOptions, ...extendedOAuthConfig.oAuthOptions],
};
};

View file

@ -2,75 +2,7 @@ import { route } from "@react-router/dev/routes";
import type { RouteConfigEntry } from "@react-router/dev/routes"; import type { RouteConfigEntry } from "@react-router/dev/routes";
import { coreRoutes } from "./routes/core"; import { coreRoutes } from "./routes/core";
import { extendedRoutes } from "./routes/extended"; import { extendedRoutes } from "./routes/extended";
import { mergeRoutes } from "./routes/helper";
/**
* Merges two route configurations intelligently.
* - Deep merges children when the same layout file exists in both arrays
* - Deduplicates routes by file property, preferring extended over core
* - Maintains order: core routes first, then extended routes at each level
*/
function mergeRoutes(core: RouteConfigEntry[], extended: RouteConfigEntry[]): RouteConfigEntry[] {
// Step 1: Create a Map to track routes by file path
const routeMap = new Map<string, RouteConfigEntry>();
// Step 2: Process core routes first
for (const coreRoute of core) {
const fileKey = coreRoute.file;
routeMap.set(fileKey, coreRoute);
}
// Step 3: Process extended routes
for (const extendedRoute of extended) {
const fileKey = extendedRoute.file;
if (routeMap.has(fileKey)) {
// Route exists in both - need to merge
const coreRoute = routeMap.get(fileKey)!;
// Check if both have children (layouts that need deep merging)
if (coreRoute.children && extendedRoute.children) {
// Deep merge: recursively merge children
const mergedChildren = mergeRoutes(
Array.isArray(coreRoute.children) ? coreRoute.children : [],
Array.isArray(extendedRoute.children) ? extendedRoute.children : []
);
routeMap.set(fileKey, {
...extendedRoute,
children: mergedChildren,
});
} else {
// No children or only one has children - prefer extended
routeMap.set(fileKey, extendedRoute);
}
} else {
// Route only exists in extended
routeMap.set(fileKey, extendedRoute);
}
}
// Step 4: Build final array maintaining order (core first, then extended-only)
const result: RouteConfigEntry[] = [];
// Add all core routes (now merged or original)
for (const coreRoute of core) {
const fileKey = coreRoute.file;
if (routeMap.has(fileKey)) {
result.push(routeMap.get(fileKey)!);
routeMap.delete(fileKey); // Remove so we don't add it again
}
}
// Add remaining extended-only routes
for (const extendedRoute of extended) {
const fileKey = extendedRoute.file;
if (routeMap.has(fileKey)) {
result.push(routeMap.get(fileKey)!);
routeMap.delete(fileKey);
}
}
return result;
}
/** /**
* Main Routes Configuration * Main Routes Configuration

View file

@ -0,0 +1,70 @@
import type { RouteConfigEntry } from "@react-router/dev/routes";
/**
* Merges two route configurations intelligently.
* - Deep merges children when the same layout file exists in both arrays
* - Deduplicates routes by file property, preferring extended over core
* - Maintains order: core routes first, then extended routes at each level
*/
export function mergeRoutes(core: RouteConfigEntry[], extended: RouteConfigEntry[]): RouteConfigEntry[] {
// Step 1: Create a Map to track routes by file path
const routeMap = new Map<string, RouteConfigEntry>();
// Step 2: Process core routes first
for (const coreRoute of core) {
const fileKey = coreRoute.file;
routeMap.set(fileKey, coreRoute);
}
// Step 3: Process extended routes
for (const extendedRoute of extended) {
const fileKey = extendedRoute.file;
if (routeMap.has(fileKey)) {
// Route exists in both - need to merge
const coreRoute = routeMap.get(fileKey)!;
// Check if both have children (layouts that need deep merging)
if (coreRoute.children && extendedRoute.children) {
// Deep merge: recursively merge children
const mergedChildren = mergeRoutes(
Array.isArray(coreRoute.children) ? coreRoute.children : [],
Array.isArray(extendedRoute.children) ? extendedRoute.children : []
);
routeMap.set(fileKey, {
...extendedRoute,
children: mergedChildren,
});
} else {
// No children or only one has children - prefer extended
routeMap.set(fileKey, extendedRoute);
}
} else {
// Route only exists in extended
routeMap.set(fileKey, extendedRoute);
}
}
// Step 4: Build final array maintaining order (core first, then extended-only)
const result: RouteConfigEntry[] = [];
// Add all core routes (now merged or original)
for (const coreRoute of core) {
const fileKey = coreRoute.file;
if (routeMap.has(fileKey)) {
result.push(routeMap.get(fileKey)!);
routeMap.delete(fileKey); // Remove so we don't add it again
}
}
// Add remaining extended-only routes
for (const extendedRoute of extended) {
const fileKey = extendedRoute.file;
if (routeMap.has(fileKey)) {
result.push(routeMap.get(fileKey)!);
routeMap.delete(fileKey);
}
}
return result;
}

View file

@ -1,77 +0,0 @@
// plane imports
import { API_BASE_URL } from "@plane/constants";
import type { TOAuthOption } from "@plane/ui";
// assets
import GithubLightLogo from "@/app/assets/logos/github-black.png?url";
import GithubDarkLogo from "@/app/assets/logos/github-dark.svg?url";
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
import GiteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
import type { IInstanceConfig } from "@plane/types";
export type OAuthConfigParams = {
OauthButtonContent: "Sign up" | "Sign in";
next_path: string | null;
config: IInstanceConfig | undefined;
resolvedTheme: string | undefined;
};
export const isOAuthEnabled = (config: IInstanceConfig | undefined) =>
(config &&
(config?.is_google_enabled ||
config?.is_github_enabled ||
config?.is_gitlab_enabled ||
config?.is_gitea_enabled)) ||
false;
export function OAUTH_CONFIG({
OauthButtonContent,
next_path,
config,
resolvedTheme,
}: OAuthConfigParams): TOAuthOption[] {
return [
{
id: "google",
text: `${OauthButtonContent} with Google`,
icon: <img src={GoogleLogo} className="h-4 w-4 object-contain" alt="Google Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/google/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_google_enabled || false,
},
{
id: "github",
text: `${OauthButtonContent} with GitHub`,
icon: (
<img
src={resolvedTheme === "dark" ? GithubDarkLogo : GithubLightLogo}
className="h-4 w-4 object-contain"
alt="GitHub Logo"
/>
),
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/github/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_github_enabled || false,
},
{
id: "gitlab",
text: `${OauthButtonContent} with GitLab`,
icon: <img src={GitlabLogo} className="h-4 w-4 object-contain" alt="GitLab Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/gitlab/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_gitlab_enabled || false,
},
{
id: "gitea",
text: `${OauthButtonContent} with Gitea`,
icon: <img src={GiteaLogo} className="h-4 w-4 object-contain" alt="Gitea Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/gitea/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_gitea_enabled || false,
},
];
}

View file

@ -1,23 +1,21 @@
import type { FC } from "react";
import { Info } from "lucide-react"; import { Info } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { CloseIcon } from "@plane/propel/icons"; import { CloseIcon } from "@plane/propel/icons";
// plane imports
// helpers // helpers
import type { TAuthErrorInfo } from "@/helpers/authentication.helper"; import type React from "react";
type TAuthBanner = { type TAuthBanner = {
bannerData: TAuthErrorInfo | undefined; message: React.ReactNode;
handleBannerData?: (bannerData: TAuthErrorInfo | undefined) => void; handleBannerData?: (bannerData: undefined) => void;
}; };
export function AuthBanner(props: TAuthBanner) { export function AuthBanner(props: TAuthBanner) {
const { bannerData, handleBannerData } = props; const { message, handleBannerData } = props;
// translation // translation
const { t } = useTranslation(); const { t } = useTranslation();
if (!bannerData) return <></>; if (!message) return <></>;
return ( return (
<div <div
role="alert" role="alert"
@ -26,7 +24,7 @@ export function AuthBanner(props: TAuthBanner) {
<div className="size-4 flex-shrink-0 grid place-items-center"> <div className="size-4 flex-shrink-0 grid place-items-center">
<Info size={16} className="text-accent-primary" /> <Info size={16} className="text-accent-primary" />
</div> </div>
<p className="w-full text-13 font-medium text-accent-primary">{bannerData?.message}</p> <p className="w-full text-13 font-medium text-accent-primary">{message}</p>
<button <button
type="button" type="button"
className="relative ml-auto size-6 rounded-xs grid place-items-center transition-all hover:bg-accent-primary/20 text-accent-primary/80" className="relative ml-auto size-6 rounded-xs grid place-items-center transition-all hover:bg-accent-primary/20 text-accent-primary/80"

View file

@ -1,4 +1,3 @@
import type { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
@ -8,8 +7,8 @@ import { LogoSpinner } from "@/components/common/logo-spinner";
import { WorkspaceLogo } from "@/components/workspace/logo"; import { WorkspaceLogo } from "@/components/workspace/logo";
// helpers // helpers
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper"; import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
import { WorkspaceService } from "@/plane-web/services";
// services // services
import { WorkspaceService } from "@/plane-web/services";
type TAuthHeader = { type TAuthHeader = {
workspaceSlug: string | undefined; workspaceSlug: string | undefined;
@ -101,10 +100,19 @@ export const AuthHeader = observer(function AuthHeader(props: TAuthHeader) {
</div> </div>
); );
return <AuthHeaderBase subHeader={subHeader} header={header} />;
});
type TAuthHeaderBase = {
header: React.ReactNode;
subHeader: string;
};
export function AuthHeaderBase(props: TAuthHeaderBase) {
return ( return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="text-h4-semibold text-primary">{typeof header === "string" ? t(header) : header}</span> <span className="text-h4-semibold text-primary">{props.header}</span>
<span className="text-h4-semibold text-placeholder">{subHeader}</span> <span className="text-h4-semibold text-placeholder">{props.subHeader}</span>
</div> </div>
); );
}); }

View file

@ -1,7 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { useTheme } from "next-themes";
// plane imports // plane imports
import { OAuthOptions } from "@plane/ui"; import { OAuthOptions } from "@plane/ui";
// helpers // helpers
@ -14,14 +13,12 @@ import {
authErrorHandler, authErrorHandler,
} from "@/helpers/authentication.helper"; } from "@/helpers/authentication.helper";
// hooks // hooks
import { useInstance } from "@/hooks/store/use-instance"; import { useOAuthConfig } from "@/hooks/oauth";
// local imports // local imports
import { TermsAndConditions } from "../terms-and-conditions"; import { TermsAndConditions } from "../terms-and-conditions";
import { AuthBanner } from "./auth-banner"; import { AuthBanner } from "./auth-banner";
import { AuthHeader } from "./auth-header"; import { AuthHeader } from "./auth-header";
import { AuthFormRoot } from "./form-root"; import { AuthFormRoot } from "./form-root";
// plane web imports
import { OAUTH_CONFIG, isOAuthEnabled as isOAuthEnabledHelper } from "@/plane-web/helpers/oauth-config";
type TAuthRoot = { type TAuthRoot = {
authMode: EAuthModes; authMode: EAuthModes;
@ -35,8 +32,6 @@ export const AuthRoot = observer(function AuthRoot(props: TAuthRoot) {
const invitation_id = searchParams.get("invitation_id"); const invitation_id = searchParams.get("invitation_id");
const workspaceSlug = searchParams.get("slug"); const workspaceSlug = searchParams.get("slug");
const error_code = searchParams.get("error_code"); const error_code = searchParams.get("error_code");
const next_path = searchParams.get("next_path");
const { resolvedTheme } = useTheme();
// props // props
const { authMode: currentAuthMode } = props; const { authMode: currentAuthMode } = props;
// states // states
@ -44,12 +39,9 @@ export const AuthRoot = observer(function AuthRoot(props: TAuthRoot) {
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL); const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
const [email, setEmail] = useState(emailParam ? emailParam.toString() : ""); const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined); const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
// hooks
const { config } = useInstance();
// derived values // derived values
const isOAuthEnabled = isOAuthEnabledHelper(config); const oAuthActionText = authMode === EAuthModes.SIGN_UP ? "Sign up" : "Sign in";
const { isOAuthEnabled, oAuthOptions } = useOAuthConfig(oAuthActionText);
useEffect(() => { useEffect(() => {
if (!authMode && currentAuthMode) setAuthMode(currentAuthMode); if (!authMode && currentAuthMode) setAuthMode(currentAuthMode);
@ -99,15 +91,11 @@ export const AuthRoot = observer(function AuthRoot(props: TAuthRoot) {
if (!authMode) return <></>; if (!authMode) return <></>;
const OauthButtonContent = authMode === EAuthModes.SIGN_UP ? "Sign up" : "Sign in";
const OAuthConfig = OAUTH_CONFIG({ OauthButtonContent, next_path, config, resolvedTheme });
return ( return (
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10"> <div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full"> <div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && ( {errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} /> <AuthBanner message={errorInfo.message} handleBannerData={(value) => setErrorInfo(value)} />
)} )}
<AuthHeader <AuthHeader
workspaceSlug={workspaceSlug?.toString() || undefined} workspaceSlug={workspaceSlug?.toString() || undefined}
@ -117,7 +105,7 @@ export const AuthRoot = observer(function AuthRoot(props: TAuthRoot) {
currentAuthStep={authStep} currentAuthStep={authStep}
/> />
{isOAuthEnabled && <OAuthOptions options={OAuthConfig} compact={authStep === EAuthSteps.PASSWORD} />} {isOAuthEnabled && <OAuthOptions options={oAuthOptions} compact={authStep === EAuthSteps.PASSWORD} />}
<AuthFormRoot <AuthFormRoot
authStep={authStep} authStep={authStep}

View file

@ -96,7 +96,7 @@ export const ResetPasswordForm = observer(function ResetPasswordForm() {
<AuthFormHeader title="Reset password" description="Create a new password." /> <AuthFormHeader title="Reset password" description="Create a new password." />
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && ( {errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} /> <AuthBanner message={errorInfo.message} handleBannerData={(value) => setErrorInfo(value)} />
)} )}
<form <form
className="space-y-4" className="space-y-4"

View file

@ -33,14 +33,12 @@ export const AuthHeader = observer(function AuthHeader({ type }: AuthHeaderProps
const { config } = useInstance(); const { config } = useInstance();
// derived values // derived values
const enableSignUpConfig = config?.enable_signup ?? false; const enableSignUpConfig = config?.enable_signup ?? false;
return ( return (
<> <AuthHeaderBase
<PageHead title={t(authContentMap[type].pageTitle) + " - Plane"} /> pageTitle={t(authContentMap[type].pageTitle)}
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0"> additionalAction={
<Link href="/"> enableSignUpConfig && (
<PlaneLockup height={20} width={95} className="text-primary" />
</Link>
{enableSignUpConfig && (
<div className="flex flex-col items-end text-13 font-medium text-center sm:items-center sm:gap-2 sm:flex-row text-tertiary"> <div className="flex flex-col items-end text-13 font-medium text-center sm:items-center sm:gap-2 sm:flex-row text-tertiary">
<span className="text-body-sm-regular text-tertiary">{t(authContentMap[type].text)}</span> <span className="text-body-sm-regular text-tertiary">{t(authContentMap[type].text)}</span>
<Link <Link
@ -51,8 +49,28 @@ export const AuthHeader = observer(function AuthHeader({ type }: AuthHeaderProps
{t(authContentMap[type].linkText)} {t(authContentMap[type].linkText)}
</Link> </Link>
</div> </div>
)} )
}
/>
);
});
type TAuthHeaderBase = {
pageTitle: string;
additionalAction?: React.ReactNode;
};
export function AuthHeaderBase(props: TAuthHeaderBase) {
const { pageTitle, additionalAction } = props;
return (
<>
<PageHead title={pageTitle + " - Plane"} />
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
<Link href="/">
<PlaneLockup height={20} width={95} className="text-primary" />
</Link>
{additionalAction}
</div> </div>
</> </>
); );
}); }

View file

@ -0,0 +1,82 @@
// plane imports
import { useSearchParams } from "next/navigation";
import { useTheme } from "next-themes";
import { API_BASE_URL } from "@plane/constants";
import type { TOAuthConfigs, TOAuthOption } from "@plane/types";
// assets
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
import GithubLightLogo from "@/app/assets/logos/github-black.png?url";
import GithubDarkLogo from "@/app/assets/logos/github-dark.svg?url";
import gitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
import googleLogo from "@/app/assets/logos/google-logo.svg?url";
// hooks
import { useInstance } from "@/hooks/store/use-instance";
export const useCoreOAuthConfig = (oauthActionText: string): TOAuthConfigs => {
//router
const searchParams = useSearchParams();
// query params
const next_path = searchParams.get("next_path");
// theme
const { resolvedTheme } = useTheme();
// store hooks
const { config } = useInstance();
// derived values
const isOAuthEnabled =
(config &&
(config?.is_google_enabled ||
config?.is_github_enabled ||
config?.is_gitlab_enabled ||
config?.is_gitea_enabled)) ||
false;
const oAuthOptions: TOAuthOption[] = [
{
id: "google",
text: `${oauthActionText} with Google`,
icon: <img src={googleLogo} height={18} width={18} alt="Google Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/google/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_google_enabled,
},
{
id: "github",
text: `${oauthActionText} with GitHub`,
icon: (
<img
src={resolvedTheme === "dark" ? GithubDarkLogo : GithubLightLogo}
height={18}
width={18}
alt="GitHub Logo"
/>
),
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/github/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_github_enabled,
},
{
id: "gitlab",
text: `${oauthActionText} with GitLab`,
icon: <img src={gitlabLogo} height={18} width={18} alt="GitLab Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/gitlab/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_gitlab_enabled,
},
{
id: "gitea",
text: `${oauthActionText} with Gitea`,
icon: <img src={giteaLogo} height={18} width={18} alt="Gitea Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/gitea/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_gitea_enabled,
},
];
return {
isOAuthEnabled,
oAuthOptions,
};
};

View file

@ -0,0 +1,9 @@
// plane imports
import type { TOAuthConfigs } from "@plane/types";
export const useExtendedOAuthConfig = (_oauthActionText: string): TOAuthConfigs => {
return {
isOAuthEnabled: false,
oAuthOptions: [],
};
};

View file

@ -0,0 +1,14 @@
// plane imports
import type { TOAuthConfigs } from "@plane/types";
// local imports
import { useCoreOAuthConfig } from "./core";
import { useExtendedOAuthConfig } from "./extended";
export const useOAuthConfig = (oauthActionText: string = "Continue"): TOAuthConfigs => {
const coreOAuthConfig = useCoreOAuthConfig(oauthActionText);
const extendedOAuthConfig = useExtendedOAuthConfig(oauthActionText);
return {
isOAuthEnabled: coreOAuthConfig.isOAuthEnabled || extendedOAuthConfig.isOAuthEnabled,
oAuthOptions: [...coreOAuthConfig.oAuthOptions, ...extendedOAuthConfig.oAuthOptions],
};
};

View file

@ -187,7 +187,7 @@ export const useYjsSetup = ({ docId, serverUrl, authToken, onStateChange }: UseY
provider.on("close", handleClose); provider.on("close", handleClose);
setYjsSession({ provider, ydoc: provider.document as Y.Doc }); setYjsSession({ provider, ydoc: provider.document });
// Handle page visibility changes (sleep/wake, tab switching) // Handle page visibility changes (sleep/wake, tab switching)
const handleVisibilityChange = (event?: Event) => { const handleVisibilityChange = (event?: Event) => {

View file

@ -1 +1,3 @@
export type TExtendedLoginMediums = never; export type TExtendedLoginMediums = never;
export type TExtendedInstanceAuthenticationModeKeys = never;

View file

@ -1,5 +1,19 @@
import type { TExtendedInstanceAuthenticationModeKeys } from "./auth-ee";
export type TCoreInstanceAuthenticationModeKeys =
| "unique-codes"
| "passwords-login"
| "google"
| "github"
| "gitlab"
| "gitea";
export type TInstanceAuthenticationModeKeys =
| TCoreInstanceAuthenticationModeKeys
| TExtendedInstanceAuthenticationModeKeys;
export type TInstanceAuthenticationModes = { export type TInstanceAuthenticationModes = {
key: string; key: TInstanceAuthenticationModeKeys;
name: string; name: string;
description: string; description: string;
icon: React.ReactNode; icon: React.ReactNode;
@ -53,4 +67,17 @@ export type TGetBaseAuthenticationModeProps = {
resolvedTheme: string | undefined; resolvedTheme: string | undefined;
}; };
export type TOAuthOption = {
id: string;
text: string;
icon: React.ReactNode;
onClick: () => void;
enabled?: boolean;
};
export type TOAuthConfigs = {
isOAuthEnabled: boolean;
oAuthOptions: TOAuthOption[];
};
export type TCoreLoginMediums = "email" | "magic-code" | "github" | "gitlab" | "google" | "gitea"; export type TCoreLoginMediums = "email" | "magic-code" | "github" | "gitlab" | "google" | "gitea";