[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:
parent
777200db7b
commit
0c795e95ac
80 changed files with 1087 additions and 950 deletions
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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" }];
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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.
|
Set it up below and please test your settings before you save them.
|
||||||
<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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./authentication-modes";
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./upgrade-button";
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
13
apps/admin/core/components/common/header/core.ts
Normal file
13
apps/admin/core/components/common/header/core.ts
Normal 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",
|
||||||
|
};
|
||||||
1
apps/admin/core/components/common/header/extended.ts
Normal file
1
apps/admin/core/components/common/header/extended.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export const EXTENDED_HEADER_SEGMENT_LABELS: Record<string, string> = {};
|
||||||
|
|
@ -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">
|
||||||
44
apps/admin/core/components/common/page-wrapper.tsx
Normal file
44
apps/admin/core/components/common/page-wrapper.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
19
apps/admin/core/hooks/oauth/index.ts
Normal file
19
apps/admin/core/hooks/oauth/index.ts
Normal 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;
|
||||||
|
};
|
||||||
7
apps/admin/core/hooks/oauth/types.ts
Normal file
7
apps/admin/core/hooks/oauth/types.ts
Normal 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;
|
||||||
|
};
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
46
apps/admin/core/hooks/use-sidebar-menu/core.ts
Normal file
46
apps/admin/core/hooks/use-sidebar-menu/core.ts
Normal 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/`,
|
||||||
|
},
|
||||||
|
};
|
||||||
14
apps/admin/core/hooks/use-sidebar-menu/index.ts
Normal file
14
apps/admin/core/hooks/use-sidebar-menu/index.ts
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
8
apps/admin/core/hooks/use-sidebar-menu/types.ts
Normal file
8
apps/admin/core/hooks/use-sidebar-menu/types.ts
Normal 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;
|
||||||
|
};
|
||||||
|
|
@ -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 />
|
||||||
3
apps/admin/core/providers/extended.tsx
Normal file
3
apps/admin/core/providers/extended.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function ExtendedProviders({ children }: { children: React.ReactNode }) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
10
apps/admin/core/providers/index.tsx
Normal file
10
apps/admin/core/providers/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "ce/components/authentication/authentication-modes";
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./authentication-modes";
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "ce/components/common";
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
82
apps/space/core/hooks/oauth/core.tsx
Normal file
82
apps/space/core/hooks/oauth/core.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
7
apps/space/core/hooks/oauth/extended.tsx
Normal file
7
apps/space/core/hooks/oauth/extended.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
// plane imports
|
||||||
|
import type { TOAuthConfigs } from "@plane/types";
|
||||||
|
|
||||||
|
export const useExtendedOAuthConfig = (_oauthActionText: string): TOAuthConfigs => ({
|
||||||
|
isOAuthEnabled: false,
|
||||||
|
oAuthOptions: [],
|
||||||
|
});
|
||||||
14
apps/space/core/hooks/oauth/index.ts
Normal file
14
apps/space/core/hooks/oauth/index.ts
Normal 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],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
70
apps/web/app/routes/helper.ts
Normal file
70
apps/web/app/routes/helper.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
|
||||||
82
apps/web/core/hooks/oauth/core.tsx
Normal file
82
apps/web/core/hooks/oauth/core.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
9
apps/web/core/hooks/oauth/extended.tsx
Normal file
9
apps/web/core/hooks/oauth/extended.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
// plane imports
|
||||||
|
import type { TOAuthConfigs } from "@plane/types";
|
||||||
|
|
||||||
|
export const useExtendedOAuthConfig = (_oauthActionText: string): TOAuthConfigs => {
|
||||||
|
return {
|
||||||
|
isOAuthEnabled: false,
|
||||||
|
oAuthOptions: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
14
apps/web/core/hooks/oauth/index.ts
Normal file
14
apps/web/core/hooks/oauth/index.ts
Normal 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],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
export type TExtendedLoginMediums = never;
|
export type TExtendedLoginMediums = never;
|
||||||
|
|
||||||
|
export type TExtendedInstanceAuthenticationModeKeys = never;
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue