[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 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}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</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">
|
||||
<Lightbulb height="14" width="14" />
|
||||
<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 className="size-4" />
|
||||
<div>
|
||||
If you have a preferred AI models vendor, please get in{" "}
|
||||
<a className="underline font-medium" href="https://plane.so/contact">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// components
|
||||
// types
|
||||
import type { Route } from "./+types/page";
|
||||
// local
|
||||
import { InstanceAIForm } from "./form";
|
||||
|
||||
const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentProps) {
|
||||
|
|
@ -14,30 +17,25 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
|
|||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-18 font-medium text-primary">AI features for all your workspaces</div>
|
||||
<div className="text-13 font-regular text-tertiary">
|
||||
Configure your AI API credentials so Plane AI features are turned on for all your workspaces.
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: "AI features for all your workspaces",
|
||||
description: "Configure your AI API credentials so Plane AI features are turned on for all your workspaces.",
|
||||
}}
|
||||
>
|
||||
{formattedConfig ? (
|
||||
<InstanceAIForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<div className="w-2/3 grid grid-cols-2 gap-x-8 gap-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceAIForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<div className="w-2/3 grid grid-cols-2 gap-x-8 gap-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</div>
|
||||
<Loader.Item height="50px" width="20%" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<Loader.Item height="50px" width="20%" />
|
||||
</Loader>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
|||
loading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</Button>
|
||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||
Go back
|
||||
|
|
@ -205,7 +205,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
{GITEA_SERVICE_FIELD.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
|
|
|
|||
|
|
@ -4,13 +4,16 @@ import useSWR from "swr";
|
|||
// plane internal packages
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
// assets
|
||||
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
|
||||
// components
|
||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
//local components
|
||||
// types
|
||||
import type { Route } from "./+types/page";
|
||||
// local
|
||||
import { InstanceGiteaConfigForm } from "./form";
|
||||
|
||||
const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthenticationPage() {
|
||||
|
|
@ -32,7 +35,7 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
|
|||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||
|
||||
setPromiseToast(updateConfigPromise, {
|
||||
loading: "Saving Configuration...",
|
||||
loading: "Saving Configuration",
|
||||
success: {
|
||||
title: "Configuration saved",
|
||||
message: () => `Gitea authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
||||
|
|
@ -56,42 +59,39 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
|
|||
const isGiteaEnabled = enableGiteaConfig === "1";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<AuthenticationMethodCard
|
||||
name="Gitea"
|
||||
description="Allow members to login or sign up to plane with their Gitea accounts."
|
||||
icon={<img src={giteaLogo} height={24} width={24} alt="Gitea Logo" />}
|
||||
config={
|
||||
<ToggleSwitch
|
||||
value={isGiteaEnabled}
|
||||
onChange={() => {
|
||||
updateConfig("IS_GITEA_ENABLED", isGiteaEnabled ? "0" : "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
/>
|
||||
}
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
withBorder={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceGiteaConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="25%" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<PageWrapper
|
||||
customHeader={
|
||||
<AuthenticationMethodCard
|
||||
name="Gitea"
|
||||
description="Allow members to login or sign up to plane with their Gitea accounts."
|
||||
icon={<img src={giteaLogo} height={24} width={24} alt="Gitea Logo" />}
|
||||
config={
|
||||
<ToggleSwitch
|
||||
value={isGiteaEnabled}
|
||||
onChange={() => {
|
||||
updateConfig("IS_GITEA_ENABLED", isGiteaEnabled ? "0" : "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
/>
|
||||
}
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
withBorder={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{formattedConfig ? (
|
||||
<InstanceGiteaConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="25%" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
</Loader>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
export const meta: Route.MetaFunction = () => [{ title: "Gitea Authentication - God Mode" }];
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
|||
loading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</Button>
|
||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||
Go back
|
||||
|
|
@ -238,7 +238,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
|||
|
||||
{/* web service details */}
|
||||
<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" />
|
||||
Web
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,15 +6,17 @@ import useSWR from "swr";
|
|||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
// components
|
||||
// assets
|
||||
import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
|
||||
import githubDarkModeImage from "@/app/assets/logos/github-white.png?url";
|
||||
// components
|
||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// icons
|
||||
// local components
|
||||
// types
|
||||
import type { Route } from "./+types/page";
|
||||
// local
|
||||
import { InstanceGithubConfigForm } from "./form";
|
||||
|
||||
const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthenticationPage(
|
||||
|
|
@ -41,7 +43,7 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
|
|||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||
|
||||
setPromiseToast(updateConfigPromise, {
|
||||
loading: "Saving Configuration...",
|
||||
loading: "Saving Configuration",
|
||||
success: {
|
||||
title: "Configuration saved",
|
||||
message: () => `GitHub authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
||||
|
|
@ -65,49 +67,46 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
|
|||
const isGithubEnabled = enableGithubConfig === "1";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<AuthenticationMethodCard
|
||||
name="GitHub"
|
||||
description="Allow members to login or sign up to plane with their GitHub accounts."
|
||||
icon={
|
||||
<img
|
||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
|
||||
height={24}
|
||||
width={24}
|
||||
alt="GitHub Logo"
|
||||
/>
|
||||
}
|
||||
config={
|
||||
<ToggleSwitch
|
||||
value={isGithubEnabled}
|
||||
onChange={() => {
|
||||
updateConfig("IS_GITHUB_ENABLED", isGithubEnabled ? "0" : "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
/>
|
||||
}
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
withBorder={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceGithubConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="25%" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<PageWrapper
|
||||
customHeader={
|
||||
<AuthenticationMethodCard
|
||||
name="GitHub"
|
||||
description="Allow members to login or sign up to plane with their GitHub accounts."
|
||||
icon={
|
||||
<img
|
||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
|
||||
height={24}
|
||||
width={24}
|
||||
alt="GitHub Logo"
|
||||
/>
|
||||
}
|
||||
config={
|
||||
<ToggleSwitch
|
||||
value={isGithubEnabled}
|
||||
onChange={() => {
|
||||
updateConfig("IS_GITHUB_ENABLED", isGithubEnabled ? "0" : "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
/>
|
||||
}
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
withBorder={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{formattedConfig ? (
|
||||
<InstanceGithubConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="25%" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
</Loader>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
|||
loading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</Button>
|
||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||
Go back
|
||||
|
|
@ -209,7 +209,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
{GITLAB_SERVICE_FIELD.map((field) => (
|
||||
<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 { setPromiseToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
// assets
|
||||
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
|
||||
// components
|
||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// icons
|
||||
// local components
|
||||
// types
|
||||
import type { Route } from "./+types/page";
|
||||
// local
|
||||
import { InstanceGitlabConfigForm } from "./form";
|
||||
|
||||
const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthenticationPage(
|
||||
|
|
@ -35,7 +37,7 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
|
|||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||
|
||||
setPromiseToast(updateConfigPromise, {
|
||||
loading: "Saving Configuration...",
|
||||
loading: "Saving Configuration",
|
||||
success: {
|
||||
title: "Configuration saved",
|
||||
message: () => `GitLab authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
||||
|
|
@ -56,46 +58,43 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
|
|||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<AuthenticationMethodCard
|
||||
name="GitLab"
|
||||
description="Allow members to login or sign up to plane with their GitLab accounts."
|
||||
icon={<img src={GitlabLogo} height={24} width={24} alt="GitLab Logo" />}
|
||||
config={
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGitlabConfig))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(enableGitlabConfig)) === true) {
|
||||
updateConfig("IS_GITLAB_ENABLED", "0");
|
||||
} else {
|
||||
updateConfig("IS_GITLAB_ENABLED", "1");
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
/>
|
||||
}
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
withBorder={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceGitlabConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="25%" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<PageWrapper
|
||||
customHeader={
|
||||
<AuthenticationMethodCard
|
||||
name="GitLab"
|
||||
description="Allow members to login or sign up to plane with their GitLab accounts."
|
||||
icon={<img src={GitlabLogo} height={24} width={24} alt="GitLab Logo" />}
|
||||
config={
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGitlabConfig))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(enableGitlabConfig)) === true) {
|
||||
updateConfig("IS_GITLAB_ENABLED", "0");
|
||||
} else {
|
||||
updateConfig("IS_GITLAB_ENABLED", "1");
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
/>
|
||||
}
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
withBorder={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{formattedConfig ? (
|
||||
<InstanceGitlabConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="25%" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
</Loader>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
|||
loading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</Button>
|
||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||
Go back
|
||||
|
|
@ -226,7 +226,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
|||
|
||||
{/* web service details */}
|
||||
<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" />
|
||||
Web
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@ import { observer } from "mobx-react";
|
|||
import useSWR from "swr";
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
// assets
|
||||
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
|
||||
// components
|
||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// icons
|
||||
// local components
|
||||
// types
|
||||
import type { Route } from "./+types/page";
|
||||
// local
|
||||
import { InstanceGoogleConfigForm } from "./form";
|
||||
|
||||
const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthenticationPage(
|
||||
|
|
@ -35,7 +37,7 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
|
|||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||
|
||||
setPromiseToast(updateConfigPromise, {
|
||||
loading: "Saving Configuration...",
|
||||
loading: "Saving Configuration",
|
||||
success: {
|
||||
title: "Configuration saved",
|
||||
message: () => `Google authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
||||
|
|
@ -56,47 +58,44 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
|
|||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<AuthenticationMethodCard
|
||||
name="Google"
|
||||
description="Allow members to login or sign up to plane with their Google
|
||||
<PageWrapper
|
||||
customHeader={
|
||||
<AuthenticationMethodCard
|
||||
name="Google"
|
||||
description="Allow members to login or sign up to plane with their Google
|
||||
accounts."
|
||||
icon={<img src={GoogleLogo} height={24} width={24} alt="Google Logo" />}
|
||||
config={
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGoogleConfig))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(enableGoogleConfig)) === true) {
|
||||
updateConfig("IS_GOOGLE_ENABLED", "0");
|
||||
} else {
|
||||
updateConfig("IS_GOOGLE_ENABLED", "1");
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
/>
|
||||
}
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
withBorder={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceGoogleConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="25%" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
icon={<img src={GoogleLogo} height={24} width={24} alt="Google Logo" />}
|
||||
config={
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGoogleConfig))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(enableGoogleConfig)) === true) {
|
||||
updateConfig("IS_GOOGLE_ENABLED", "0");
|
||||
} else {
|
||||
updateConfig("IS_GOOGLE_ENABLED", "1");
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
/>
|
||||
}
|
||||
disabled={isSubmitting || !formattedConfig}
|
||||
withBorder={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{formattedConfig ? (
|
||||
<InstanceGoogleConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="25%" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
</Loader>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,33 @@
|
|||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
// plane internal packages
|
||||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import type { TInstanceConfigurationKeys } from "@plane/types";
|
||||
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
|
||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||
import { useAuthenticationModes } from "@/hooks/oauth";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// plane admin components
|
||||
import { AuthenticationModes } from "@/plane-admin/components/authentication";
|
||||
// types
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(_props: Route.ComponentProps) {
|
||||
// theme
|
||||
const { resolvedTheme: resolvedThemeAdmin } = useTheme();
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
|
||||
|
||||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||
|
||||
// state
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
// derived values
|
||||
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
|
||||
const resolvedTheme = resolveGeneralTheme(resolvedThemeAdmin);
|
||||
|
||||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||
|
||||
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
|
||||
setIsSubmitting(true);
|
||||
|
|
@ -54,59 +60,65 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
|||
});
|
||||
};
|
||||
|
||||
const authenticationModes = useAuthenticationModes({ disabled: isSubmitting, updateConfig, resolvedTheme });
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-18 font-medium text-primary">Manage authentication modes for your instance</div>
|
||||
<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 ? (
|
||||
<div className="space-y-3">
|
||||
<div className={cn("w-full flex items-center gap-14 rounded-sm")}>
|
||||
<div className="flex grow items-center gap-4">
|
||||
<div className="grow">
|
||||
<div className="text-16 font-medium pb-1">Allow anyone to sign up even without an invite</div>
|
||||
<div className={cn("font-regular leading-5 text-tertiary text-11")}>
|
||||
Toggling this off will only let users sign up when they are invited.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableSignUpConfig))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(enableSignUpConfig)) === true) {
|
||||
updateConfig("ENABLE_SIGNUP", "0");
|
||||
} else {
|
||||
updateConfig("ENABLE_SIGNUP", "1");
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: "Manage authentication modes for your instance",
|
||||
description: "Configure authentication modes for your team and restrict sign-ups to be invite only.",
|
||||
}}
|
||||
>
|
||||
{formattedConfig ? (
|
||||
<div className="space-y-3">
|
||||
<div className={cn("w-full flex items-center gap-14 rounded-sm")}>
|
||||
<div className="flex grow items-center gap-4">
|
||||
<div className="grow">
|
||||
<div className="text-16 font-medium pb-1">Allow anyone to sign up even without an invite</div>
|
||||
<div className={cn("font-regular leading-5 text-tertiary text-11")}>
|
||||
Toggling this off will only let users sign up when they are invited.
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-16 font-medium pt-6">Available authentication modes</div>
|
||||
<AuthenticationModes disabled={isSubmitting} updateConfig={updateConfig} />
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="space-y-10">
|
||||
<Loader.Item height="50px" width="75%" />
|
||||
<Loader.Item height="50px" width="75%" />
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<Loader.Item height="50px" width="20%" />
|
||||
</Loader>
|
||||
)}
|
||||
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableSignUpConfig))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(enableSignUpConfig)) === true) {
|
||||
updateConfig("ENABLE_SIGNUP", "0");
|
||||
} else {
|
||||
updateConfig("ENABLE_SIGNUP", "1");
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-lg font-medium pt-6">Available authentication modes</div>
|
||||
{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.Item height="50px" width="75%" />
|
||||
<Loader.Item height="50px" width="75%" />
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<Loader.Item height="50px" width="20%" />
|
||||
</Loader>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
|||
loading={isSubmitting}
|
||||
disabled={!isValid || !isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
|
|
|||
|
|
@ -3,10 +3,13 @@ import { observer } from "mobx-react";
|
|||
import useSWR from "swr";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// components
|
||||
// types
|
||||
import type { Route } from "./+types/page";
|
||||
// local
|
||||
import { InstanceEmailForm } from "./email-config-form";
|
||||
|
||||
const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.ComponentProps) {
|
||||
|
|
@ -49,44 +52,43 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
|
|||
}, [formattedConfig]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: "Secure emails from your own instance",
|
||||
description: (
|
||||
<>
|
||||
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">
|
||||
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">
|
||||
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>
|
||||
</div>
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Loader>
|
||||
<Loader.Item width="24px" height="16px" className="rounded-full" />
|
||||
</Loader>
|
||||
</>
|
||||
),
|
||||
actions: isLoading ? (
|
||||
<Loader>
|
||||
<Loader.Item width="24px" height="16px" className="rounded-full" />
|
||||
</Loader>
|
||||
) : (
|
||||
<ToggleSwitch value={isSMTPEnabled} onChange={handleToggle} size="sm" disabled={isSubmitting} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{isSMTPEnabled && !isLoading && (
|
||||
<>
|
||||
{formattedConfig ? (
|
||||
<InstanceEmailForm config={formattedConfig} />
|
||||
) : (
|
||||
<ToggleSwitch value={isSMTPEnabled} onChange={handleToggle} size="sm" disabled={isSubmitting} />
|
||||
<Loader className="space-y-10">
|
||||
<Loader.Item height="50px" width="75%" />
|
||||
<Loader.Item height="50px" width="75%" />
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<Loader.Item height="50px" width="20%" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
{isSMTPEnabled && !isLoading && (
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceEmailForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-10">
|
||||
<Loader.Item height="50px" width="75%" />
|
||||
<Loader.Item height="50px" width="75%" />
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<Loader.Item height="50px" width="40%" />
|
||||
<Loader.Item height="50px" width="20%" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export function SendTestEmailModal(props: Props) {
|
|||
</Button>
|
||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
|
||||
<Button variant="primary" size="lg" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
|
||||
{isLoading ? "Sending email..." : "Send email"}
|
||||
{isLoading ? "Sending email" : "Send email"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Telescope } from "lucide-react";
|
||||
// types
|
||||
// plane imports
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IInstance, IInstanceAdmin } from "@plane/types";
|
||||
// ui
|
||||
import { Input, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { IntercomConfig } from "./intercom";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// components
|
||||
import { IntercomConfig } from "./intercom";
|
||||
|
||||
export interface IGeneralConfigurationForm {
|
||||
instance: IInstance;
|
||||
|
|
@ -27,8 +27,8 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
|||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
formState: { errors, isSubmitting },
|
||||
watch,
|
||||
} = useForm<Partial<IInstance>>({
|
||||
defaultValues: {
|
||||
instance_name: instance?.instance_name,
|
||||
|
|
@ -105,14 +105,14 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="text-16 font-medium text-primary">Chat + telemetry</div>
|
||||
<div className="space-y-6">
|
||||
<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} />
|
||||
<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="shrink-0">
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-layer-1 rounded-full">
|
||||
<Telescope className="w-6 h-6 text-tertiary/80 p-0.5" />
|
||||
<div className="flex items-center justify-center size-11 bg-layer-1 rounded-lg">
|
||||
<Telescope className="size-5 text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow">
|
||||
|
|
@ -144,8 +144,15 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
void handleSubmit(onSubmit)();
|
||||
}}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -44,16 +44,16 @@ export const IntercomConfig = observer(function IntercomConfig(props: TIntercomC
|
|||
};
|
||||
|
||||
const enableIntercomConfig = () => {
|
||||
submitInstanceConfigurations({ IS_INTERCOM_ENABLED: isIntercomEnabled ? "0" : "1" });
|
||||
void submitInstanceConfigurations({ IS_INTERCOM_ENABLED: isIntercomEnabled ? "0" : "1" });
|
||||
};
|
||||
|
||||
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="shrink-0">
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-layer-1 rounded-full">
|
||||
<MessageSquare className="w-6 h-6 text-tertiary/80 p-0.5" />
|
||||
<div className="flex items-center justify-center size-11 bg-layer-1 rounded-lg">
|
||||
<MessageSquare className="size-5 text-tertiary p-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,26 @@
|
|||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// components
|
||||
import type { Route } from "./+types/page";
|
||||
// local imports
|
||||
import { GeneralConfigurationForm } from "./form";
|
||||
// types
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
function GeneralPage() {
|
||||
const { instance, instanceAdmins } = useInstance();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-18 font-medium text-primary">General settings</div>
|
||||
<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.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{instance && instanceAdmins && (
|
||||
<GeneralConfigurationForm instance={instance} instanceAdmins={instanceAdmins} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: "General settings",
|
||||
description:
|
||||
"Change the name of your instance and instance admin e-mail addresses. Enable or disable telemetry in your instance.",
|
||||
}}
|
||||
>
|
||||
{instance && instanceAdmins && <GeneralConfigurationForm instance={instance} instanceAdmins={instanceAdmins} />}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
|
|||
|
||||
<div>
|
||||
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// local
|
||||
// types
|
||||
import type { Route } from "./+types/page";
|
||||
// local
|
||||
import { InstanceImageConfigForm } from "./form";
|
||||
|
||||
const InstanceImagePage = observer(function InstanceImagePage(_props: Route.ComponentProps) {
|
||||
|
|
@ -14,26 +17,21 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp
|
|||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-18 font-medium text-primary">Third-party image libraries</div>
|
||||
<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 ? (
|
||||
<InstanceImageConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
<Loader.Item height="50px" width="20%" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: "Third-party image libraries",
|
||||
description: "Let your users search and choose images from third-party libraries",
|
||||
}}
|
||||
>
|
||||
{formattedConfig ? (
|
||||
<InstanceImageConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
<Loader className="space-y-8">
|
||||
<Loader.Item height="50px" width="50%" />
|
||||
<Loader.Item height="50px" width="20%" />
|
||||
</Loader>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { observer } from "mobx-react";
|
|||
import { useRouter } from "next/navigation";
|
||||
import { Outlet } from "react-router";
|
||||
// components
|
||||
import { AdminHeader } from "@/components/common/header";
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { NewUserPopup } from "@/components/new-user-popup";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
// local components
|
||||
import type { Route } from "./+types/layout";
|
||||
import { AdminHeader } from "./header";
|
||||
import { AdminSidebar } from "./sidebar";
|
||||
|
||||
function AdminLayout(_props: Route.ComponentProps) {
|
||||
|
|
|
|||
|
|
@ -71,14 +71,14 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
|||
|
||||
useEffect(() => {
|
||||
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]);
|
||||
|
||||
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={`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" : ""
|
||||
}`}
|
||||
>
|
||||
|
|
@ -88,8 +88,8 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
|||
"cursor-default": !isSidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<div className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-sm bg-layer-1">
|
||||
<UserCog2 className="h-5 w-5 text-secondary" />
|
||||
<div className="flex size-8 flex-shrink-0 items-center justify-center rounded-sm bg-layer-1">
|
||||
<UserCog2 className="size-5 text-primary" />
|
||||
</div>
|
||||
</Menu.Button>
|
||||
{isSidebarCollapsed && (
|
||||
|
|
@ -109,7 +109,7 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
|||
|
||||
{!isSidebarCollapsed && (
|
||||
<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>
|
||||
|
|
@ -123,7 +123,7 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
|||
src={getFileURL(currentUser.avatar_url)}
|
||||
size={24}
|
||||
shape="square"
|
||||
className="!text-14"
|
||||
className="!text-body-sm-medium"
|
||||
/>
|
||||
</Menu.Button>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,9 @@ import { DiscordIcon, GithubIcon, PageIcon } from "@plane/propel/icons";
|
|||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
import { useInstance, useTheme } from "@/hooks/store";
|
||||
// assets
|
||||
|
||||
import packageJson from "package.json";
|
||||
|
||||
const helpOptions = [
|
||||
{
|
||||
name: "Documentation",
|
||||
|
|
@ -36,6 +34,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
|||
// states
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
// store
|
||||
const { instance } = useInstance();
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
// refs
|
||||
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}>
|
||||
<a
|
||||
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"}
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
|
@ -69,7 +68,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
|||
}`}
|
||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||
>
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
<HelpCircle className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip tooltipContent="Toggle sidebar" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
||||
|
|
@ -80,7 +79,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
|||
}`}
|
||||
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>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
@ -108,7 +107,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
|||
<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="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>
|
||||
<span className="text-11">{name}</span>
|
||||
</div>
|
||||
|
|
@ -129,7 +128,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
|||
);
|
||||
})}
|
||||
</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>
|
||||
</Transition>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,58 +1,20 @@
|
|||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { WorkspaceIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
|
||||
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/`,
|
||||
},
|
||||
];
|
||||
import { useSidebarMenu } from "@/hooks/use-sidebar-menu";
|
||||
|
||||
export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
|
||||
// store hooks
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
// router
|
||||
const pathName = usePathname();
|
||||
// store hooks
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
// derived values
|
||||
const sidebarMenu = useSidebarMenu();
|
||||
|
||||
const handleItemClick = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
|
|
@ -62,40 +24,27 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
|
|||
|
||||
return (
|
||||
<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) => {
|
||||
const isActive = item.href === pathName || pathName.includes(item.href);
|
||||
{sidebarMenu.map((item, index) => {
|
||||
const isActive = item.href === pathName || pathName?.includes(item.href);
|
||||
return (
|
||||
<Link key={index} href={item.href} onClick={handleItemClick}>
|
||||
<div>
|
||||
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
||||
<div
|
||||
className={cn(
|
||||
`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-secondary hover:bg-layer-1-hover focus:bg-layer-1-hover",
|
||||
"group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none transition-colors",
|
||||
{
|
||||
"text-primary !bg-layer-transparent-active": isActive,
|
||||
"text-secondary hover:bg-layer-transparent-hover active:bg-layer-transparent-active": !isActive,
|
||||
},
|
||||
isSidebarCollapsed ? "justify-center" : "w-[260px]"
|
||||
)}
|
||||
>
|
||||
{<item.Icon className="h-4 w-4 flex-shrink-0" />}
|
||||
{!isSidebarCollapsed && (
|
||||
<div className="w-full ">
|
||||
<div
|
||||
className={cn(
|
||||
`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 className={cn(`text-body-xs-medium transition-colors`)}>{item.name}</div>
|
||||
<div className={cn(`text-caption-sm-regular transition-colors`)}>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
// types
|
||||
import type { Route } from "./+types/page";
|
||||
// local
|
||||
import { WorkspaceCreateForm } from "./form";
|
||||
|
||||
const WorkspaceCreatePage = observer(function WorkspaceCreatePage(_props: Route.ComponentProps) {
|
||||
return (
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-subtle mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-18 font-medium text-primary">Create a new workspace on this instance.</div>
|
||||
<div className="text-13 font-regular text-tertiary">
|
||||
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 />
|
||||
</div>
|
||||
</div>
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: "Create a new workspace on this instance.",
|
||||
description: "You will need to invite users from Workspace Settings after you create this workspace.",
|
||||
}}
|
||||
>
|
||||
<WorkspaceCreateForm />
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@ import { Button, getButtonStyling } from "@plane/propel/button";
|
|||
import { setPromiseToast } from "@plane/propel/toast";
|
||||
import type { TInstanceConfigurationKeys } from "@plane/types";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||
import { WorkspaceListItem } from "@/components/workspace/list-item";
|
||||
// hooks
|
||||
import { useInstance, useWorkspace } from "@/hooks/store";
|
||||
// types
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props: Route.ComponentProps) {
|
||||
|
|
@ -68,99 +69,95 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
|
|||
};
|
||||
|
||||
return (
|
||||
<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="flex flex-col gap-1">
|
||||
<div className="text-18 font-medium text-primary">Workspaces on this instance</div>
|
||||
<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">
|
||||
{formattedConfig ? (
|
||||
<div className={cn("w-full flex items-center gap-14 rounded-sm")}>
|
||||
<div className="flex grow items-center gap-4">
|
||||
<div className="grow">
|
||||
<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")}>
|
||||
Toggling this on will let only you create workspaces. You will have to invite users to new
|
||||
workspaces.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(disableWorkspaceCreation))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(disableWorkspaceCreation)) === true) {
|
||||
updateConfig("DISABLE_WORKSPACE_CREATION", "0");
|
||||
} else {
|
||||
updateConfig("DISABLE_WORKSPACE_CREATION", "1");
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: "Workspaces on this instance",
|
||||
description: "See all workspaces and control who can create them.",
|
||||
}}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{formattedConfig ? (
|
||||
<div className={cn("w-full flex items-center gap-14 rounded-sm")}>
|
||||
<div className="flex grow items-center gap-4">
|
||||
<div className="grow">
|
||||
<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")}>
|
||||
Toggling this on will let only you create workspaces. You will have to invite users to new workspaces.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="50px" width="100%" />
|
||||
</Loader>
|
||||
)}
|
||||
{workspaceLoader !== "init-loader" ? (
|
||||
<>
|
||||
<div className="pt-6 flex items-center justify-between gap-2">
|
||||
<div className="flex flex-col items-start gap-x-2">
|
||||
<div className="flex items-center gap-2 text-16 font-medium">
|
||||
All workspaces on this instance <span className="text-tertiary">• {workspaceIds.length}</span>
|
||||
{workspaceLoader && ["mutation", "pagination"].includes(workspaceLoader) && (
|
||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
<div className={cn("font-regular leading-5 text-tertiary text-11")}>
|
||||
You can't yet delete workspaces and you can only go to the workspace if you are an Admin or a
|
||||
Member.
|
||||
</div>
|
||||
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(disableWorkspaceCreation))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(disableWorkspaceCreation)) === true) {
|
||||
updateConfig("DISABLE_WORKSPACE_CREATION", "0");
|
||||
} else {
|
||||
updateConfig("DISABLE_WORKSPACE_CREATION", "1");
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="50px" width="100%" />
|
||||
</Loader>
|
||||
)}
|
||||
{workspaceLoader !== "init-loader" ? (
|
||||
<>
|
||||
<div className="pt-6 flex items-center justify-between gap-2">
|
||||
<div className="flex flex-col items-start gap-x-2">
|
||||
<div className="flex items-center gap-2 text-16 font-medium">
|
||||
All workspaces on this instance <span className="text-tertiary">• {workspaceIds.length}</span>
|
||||
{workspaceLoader && ["mutation", "pagination"].includes(workspaceLoader) && (
|
||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/workspace/create" className={getButtonStyling("primary", "lg")}>
|
||||
Create workspace
|
||||
</Link>
|
||||
<div className={cn("font-regular leading-5 text-tertiary text-11")}>
|
||||
You can't yet delete workspaces and you can only go to the workspace if you are an Admin or a
|
||||
Member.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
{workspaceIds.map((workspaceId) => (
|
||||
<WorkspaceListItem key={workspaceId} workspaceId={workspaceId} />
|
||||
))}
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/workspace/create" className={getButtonStyling("primary", "base")}>
|
||||
Create workspace
|
||||
</Link>
|
||||
</div>
|
||||
{hasNextPage && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="link"
|
||||
size="lg"
|
||||
onClick={() => fetchNextWorkspaces()}
|
||||
disabled={workspaceLoader === "pagination"}
|
||||
>
|
||||
Load more
|
||||
{workspaceLoader === "pagination" && <LoaderIcon className="w-3 h-3 animate-spin" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Loader className="space-y-10 py-8">
|
||||
<Loader.Item height="24px" width="20%" />
|
||||
<Loader.Item height="92px" width="100%" />
|
||||
<Loader.Item height="92px" width="100%" />
|
||||
<Loader.Item height="92px" width="100%" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
{workspaceIds.map((workspaceId) => (
|
||||
<WorkspaceListItem key={workspaceId} workspaceId={workspaceId} />
|
||||
))}
|
||||
</div>
|
||||
{hasNextPage && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="link"
|
||||
size="lg"
|
||||
onClick={() => fetchNextWorkspaces()}
|
||||
disabled={workspaceLoader === "pagination"}
|
||||
>
|
||||
Load more
|
||||
{workspaceLoader === "pagination" && <LoaderIcon className="w-3 h-3 animate-spin" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Loader className="space-y-10 py-8">
|
||||
<Loader.Item height="24px" width="20%" />
|
||||
<Loader.Item height="92px" width="100%" />
|
||||
<Loader.Item height="92px" width="100%" />
|
||||
<Loader.Item height="92px" width="100%" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export function AuthBanner(props: TAuthBanner) {
|
|||
</div>
|
||||
<div className="w-full text-13 font-medium text-accent-primary">{bannerData?.message}</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)}
|
||||
>
|
||||
<CloseIcon className="w-4 h-4 flex-shrink-0" />
|
||||
|
|
|
|||
|
|
@ -117,14 +117,14 @@ export const getBaseAuthenticationModes: (props: TGetBaseAuthenticationModeProps
|
|||
name: "Unique codes",
|
||||
description:
|
||||
"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} />,
|
||||
},
|
||||
{
|
||||
key: "passwords-login",
|
||||
name: "Passwords",
|
||||
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} />,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import type { ReactNode } from "react";
|
||||
import * as Sentry from "@sentry/react-router";
|
||||
import { Links, Meta, Outlet, Scripts } 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 favicon16 from "@/app/assets/favicon/favicon-16x16.png?url";
|
||||
import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url";
|
||||
import faviconIco from "@/app/assets/favicon/favicon.ico?url";
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import globalStyles from "@/styles/globals.css?url";
|
||||
import { AppProviders } from "@/providers";
|
||||
import type { Route } from "./+types/root";
|
||||
import { AppProviders } from "./providers";
|
||||
// fonts
|
||||
import "@fontsource-variable/inter";
|
||||
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 (
|
||||
<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,
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const GiteaConfiguration = observer(function GiteaConfiguration(props: Pr
|
|||
</div>
|
||||
) : (
|
||||
<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
|
||||
</Link>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
// icons
|
||||
|
|
@ -43,7 +42,7 @@ export const GithubConfiguration = observer(function GithubConfiguration(props:
|
|||
</div>
|
||||
) : (
|
||||
<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
|
||||
</Link>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const GitlabConfiguration = observer(function GitlabConfiguration(props:
|
|||
</div>
|
||||
) : (
|
||||
<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
|
||||
</Link>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const GoogleConfiguration = observer(function GoogleConfiguration(props:
|
|||
</div>
|
||||
) : (
|
||||
<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
|
||||
</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";
|
||||
// hooks
|
||||
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() {
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
return (
|
||||
<div
|
||||
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"
|
||||
<button
|
||||
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)}
|
||||
>
|
||||
<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() {
|
||||
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
|
||||
const generateBreadcrumbItems = (pathname: 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) => {
|
||||
currentUrl += "/" + segment;
|
||||
return {
|
||||
title: getHeaderTitle(segment),
|
||||
title: HEADER_SEGMENT_LABELS[segment] ?? segment.toUpperCase(),
|
||||
href: currentUrl,
|
||||
};
|
||||
});
|
||||
return breadcrumbItems;
|
||||
};
|
||||
|
||||
const breadcrumbItems = generateBreadcrumbItems(pathName);
|
||||
const breadcrumbItems = generateBreadcrumbItems(pathName || "");
|
||||
|
||||
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">
|
||||
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() {
|
||||
// search params
|
||||
const searchParams = useSearchParams();
|
||||
const firstNameParam = searchParams.get("first_name") || undefined;
|
||||
const lastNameParam = searchParams.get("last_name") || undefined;
|
||||
const companyParam = searchParams.get("company") || undefined;
|
||||
const emailParam = searchParams.get("email") || undefined;
|
||||
const isTelemetryEnabledParam = (searchParams.get("is_telemetry_enabled") === "True" ? true : false) || true;
|
||||
const errorCode = searchParams.get("error_code") || undefined;
|
||||
const errorMessage = searchParams.get("error_message") || undefined;
|
||||
const firstNameParam = searchParams?.get("first_name") || undefined;
|
||||
const lastNameParam = searchParams?.get("last_name") || undefined;
|
||||
const companyParam = searchParams?.get("company") || undefined;
|
||||
const emailParam = searchParams?.get("email") || undefined;
|
||||
const isTelemetryEnabledParam = (searchParams?.get("is_telemetry_enabled") === "True" ? true : false) || true;
|
||||
const errorCode = searchParams?.get("error_code") || undefined;
|
||||
const errorMessage = searchParams?.get("error_message") || undefined;
|
||||
// state
|
||||
const [showPassword, setShowPassword] = useState({
|
||||
password: false,
|
||||
|
|
@ -238,7 +238,7 @@ export function InstanceSetupForm() {
|
|||
name="password"
|
||||
type={showPassword.password ? "text" : "password"}
|
||||
inputSize="md"
|
||||
placeholder="New password..."
|
||||
placeholder="New password"
|
||||
value={formData.password}
|
||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
|
|||
key={workspaceId}
|
||||
href={`${WEB_BASE_URL}/${encodeURIComponent(workspace.slug)}`}
|
||||
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"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<span
|
||||
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 !== "" ? (
|
||||
|
|
@ -75,7 +75,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</a>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,72 +1,61 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { KeyRound, Mails } from "lucide-react";
|
||||
// types
|
||||
import type {
|
||||
TCoreInstanceAuthenticationModeKeys,
|
||||
TGetBaseAuthenticationModeProps,
|
||||
TInstanceAuthenticationMethodKeys,
|
||||
TInstanceAuthenticationModes,
|
||||
} from "@plane/types";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
// components
|
||||
// assets
|
||||
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
|
||||
import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
|
||||
import githubDarkModeImage from "@/app/assets/logos/github-white.png?url";
|
||||
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
|
||||
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
|
||||
import OIDCLogo from "@/app/assets/logos/oidc-logo.svg?url";
|
||||
import SAMLLogo from "@/app/assets/logos/saml-logo.svg?url";
|
||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||
import gitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
|
||||
import googleLogo from "@/app/assets/logos/google-logo.svg?url";
|
||||
// components
|
||||
import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch";
|
||||
import { GiteaConfiguration } from "@/components/authentication/gitea-config";
|
||||
import { GithubConfiguration } from "@/components/authentication/github-config";
|
||||
import { GitlabConfiguration } from "@/components/authentication/gitlab-config";
|
||||
import { GoogleConfiguration } from "@/components/authentication/google-config";
|
||||
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
|
||||
export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({
|
||||
export const getCoreAuthenticationModesMap: (
|
||||
props: TGetBaseAuthenticationModeProps
|
||||
) => Record<TCoreInstanceAuthenticationModeKeys, TInstanceAuthenticationModes> = ({
|
||||
disabled,
|
||||
updateConfig,
|
||||
resolvedTheme,
|
||||
}) => [
|
||||
{
|
||||
}) => ({
|
||||
"unique-codes": {
|
||||
key: "unique-codes",
|
||||
name: "Unique codes",
|
||||
description:
|
||||
"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} />,
|
||||
},
|
||||
{
|
||||
"passwords-login": {
|
||||
key: "passwords-login",
|
||||
name: "Passwords",
|
||||
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} />,
|
||||
},
|
||||
{
|
||||
google: {
|
||||
key: "google",
|
||||
name: "Google",
|
||||
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} />,
|
||||
},
|
||||
{
|
||||
github: {
|
||||
key: "github",
|
||||
name: "GitHub",
|
||||
description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
|
||||
icon: (
|
||||
<img
|
||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
|
||||
src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
|
||||
height={20}
|
||||
width={20}
|
||||
alt="GitHub Logo"
|
||||
|
|
@ -74,56 +63,18 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
|
|||
),
|
||||
config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
gitlab: {
|
||||
key: "gitlab",
|
||||
name: "GitLab",
|
||||
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} />,
|
||||
},
|
||||
{
|
||||
gitea: {
|
||||
key: "gitea",
|
||||
name: "Gitea",
|
||||
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" />,
|
||||
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";
|
||||
// store
|
||||
import { StoreContext } from "@/app/(all)/store.provider";
|
||||
import { StoreContext } from "@/providers/store.provider";
|
||||
import type { IInstanceStore } from "@/store/instance.store";
|
||||
|
||||
export const useInstance = (): IInstanceStore => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/app/(all)/store.provider";
|
||||
import { StoreContext } from "@/providers/store.provider";
|
||||
import type { IThemeStore } from "@/store/theme.store";
|
||||
|
||||
export const useTheme = (): IThemeStore => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/app/(all)/store.provider";
|
||||
import { StoreContext } from "@/providers/store.provider";
|
||||
import type { IUserStore } from "@/store/user.store";
|
||||
|
||||
export const useUser = (): IUserStore => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/app/(all)/store.provider";
|
||||
import { StoreContext } from "@/providers/store.provider";
|
||||
import type { IWorkspaceStore } from "@/store/workspace.store";
|
||||
|
||||
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 { SWRConfig } from "swr";
|
||||
import { AppProgressBar } from "@/lib/b-progress";
|
||||
import { InstanceProvider } from "./(all)/instance.provider";
|
||||
import { StoreProvider } from "./(all)/store.provider";
|
||||
import { ToastWithTheme } from "./(all)/toast";
|
||||
import { UserProvider } from "./(all)/user.provider";
|
||||
// local imports
|
||||
import { ToastWithTheme } from "./toast";
|
||||
import { StoreProvider } from "./store.provider";
|
||||
import { InstanceProvider } from "./instance.provider";
|
||||
import { UserProvider } from "./user.provider";
|
||||
|
||||
const DEFAULT_SWR_CONFIG = {
|
||||
refreshWhenHidden: false,
|
||||
|
|
@ -15,7 +16,7 @@ const DEFAULT_SWR_CONFIG = {
|
|||
errorRetryCount: 3,
|
||||
};
|
||||
|
||||
export function AppProviders({ children }: { children: React.ReactNode }) {
|
||||
export function CoreProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||
<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 { observer } from "mobx-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { SitesAuthService } from "@plane/services";
|
||||
import type { IEmailCheckData } from "@plane/types";
|
||||
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
|
||||
import type { TAuthErrorInfo } from "@/helpers/authentication.helper";
|
||||
import { EErrorAlertType, authErrorHandler, EAuthenticationErrorCodes } from "@/helpers/authentication.helper";
|
||||
// hooks
|
||||
import { useOAuthConfig } from "@/hooks/oauth";
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
// types
|
||||
import { EAuthModes, EAuthSteps } from "@/types/auth";
|
||||
|
|
@ -36,7 +29,6 @@ export const AuthRoot = observer(function AuthRoot() {
|
|||
const emailParam = searchParams.get("email") || undefined;
|
||||
const error_code = searchParams.get("error_code") || undefined;
|
||||
const nextPath = searchParams.get("next_path") || undefined;
|
||||
const next_path = searchParams.get("next_path");
|
||||
// states
|
||||
const [authMode, setAuthMode] = useState<EAuthModes>(EAuthModes.SIGN_UP);
|
||||
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 [isPasswordAutoset, setIsPasswordAutoset] = useState(true);
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { config } = useInstance();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -87,13 +78,8 @@ export const AuthRoot = observer(function AuthRoot() {
|
|||
const isSMTPConfigured = config?.is_smtp_configured || false;
|
||||
const isMagicLoginEnabled = config?.is_magic_login_enabled || false;
|
||||
const isEmailPasswordEnabled = config?.is_email_password_enabled || false;
|
||||
const isOAuthEnabled =
|
||||
(config &&
|
||||
(config?.is_google_enabled ||
|
||||
config?.is_github_enabled ||
|
||||
config?.is_gitlab_enabled ||
|
||||
config?.is_gitea_enabled)) ||
|
||||
false;
|
||||
const oAuthActionText = authMode === EAuthModes.SIGN_UP ? "Sign up" : "Sign in";
|
||||
const { isOAuthEnabled, oAuthOptions } = useOAuthConfig(oAuthActionText);
|
||||
|
||||
// submit handler- email verification
|
||||
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 (
|
||||
<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">
|
||||
|
|
@ -208,7 +146,7 @@ export const AuthRoot = observer(function AuthRoot() {
|
|||
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
|
||||
)}
|
||||
<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.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 { coreRoutes } from "./routes/core";
|
||||
import { extendedRoutes } from "./routes/extended";
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
import { mergeRoutes } from "./routes/helper";
|
||||
|
||||
/**
|
||||
* 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";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// plane imports
|
||||
// helpers
|
||||
import type { TAuthErrorInfo } from "@/helpers/authentication.helper";
|
||||
import type React from "react";
|
||||
|
||||
type TAuthBanner = {
|
||||
bannerData: TAuthErrorInfo | undefined;
|
||||
handleBannerData?: (bannerData: TAuthErrorInfo | undefined) => void;
|
||||
message: React.ReactNode;
|
||||
handleBannerData?: (bannerData: undefined) => void;
|
||||
};
|
||||
|
||||
export function AuthBanner(props: TAuthBanner) {
|
||||
const { bannerData, handleBannerData } = props;
|
||||
const { message, handleBannerData } = props;
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!bannerData) return <></>;
|
||||
|
||||
if (!message) return <></>;
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
|
|
@ -26,7 +24,7 @@ export function AuthBanner(props: TAuthBanner) {
|
|||
<div className="size-4 flex-shrink-0 grid place-items-center">
|
||||
<Info size={16} className="text-accent-primary" />
|
||||
</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
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
|
@ -8,8 +7,8 @@ import { LogoSpinner } from "@/components/common/logo-spinner";
|
|||
import { WorkspaceLogo } from "@/components/workspace/logo";
|
||||
// helpers
|
||||
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
|
||||
type TAuthHeader = {
|
||||
workspaceSlug: string | undefined;
|
||||
|
|
@ -101,10 +100,19 @@ export const AuthHeader = observer(function AuthHeader(props: TAuthHeader) {
|
|||
</div>
|
||||
);
|
||||
|
||||
return <AuthHeaderBase subHeader={subHeader} header={header} />;
|
||||
});
|
||||
|
||||
type TAuthHeaderBase = {
|
||||
header: React.ReactNode;
|
||||
subHeader: string;
|
||||
};
|
||||
|
||||
export function AuthHeaderBase(props: TAuthHeaderBase) {
|
||||
return (
|
||||
<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-placeholder">{subHeader}</span>
|
||||
<span className="text-h4-semibold text-primary">{props.header}</span>
|
||||
<span className="text-h4-semibold text-placeholder">{props.subHeader}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
// plane imports
|
||||
import { OAuthOptions } from "@plane/ui";
|
||||
// helpers
|
||||
|
|
@ -14,14 +13,12 @@ import {
|
|||
authErrorHandler,
|
||||
} from "@/helpers/authentication.helper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
import { useOAuthConfig } from "@/hooks/oauth";
|
||||
// local imports
|
||||
import { TermsAndConditions } from "../terms-and-conditions";
|
||||
import { AuthBanner } from "./auth-banner";
|
||||
import { AuthHeader } from "./auth-header";
|
||||
import { AuthFormRoot } from "./form-root";
|
||||
// plane web imports
|
||||
import { OAUTH_CONFIG, isOAuthEnabled as isOAuthEnabledHelper } from "@/plane-web/helpers/oauth-config";
|
||||
|
||||
type TAuthRoot = {
|
||||
authMode: EAuthModes;
|
||||
|
|
@ -35,8 +32,6 @@ export const AuthRoot = observer(function AuthRoot(props: TAuthRoot) {
|
|||
const invitation_id = searchParams.get("invitation_id");
|
||||
const workspaceSlug = searchParams.get("slug");
|
||||
const error_code = searchParams.get("error_code");
|
||||
const next_path = searchParams.get("next_path");
|
||||
const { resolvedTheme } = useTheme();
|
||||
// props
|
||||
const { authMode: currentAuthMode } = props;
|
||||
// states
|
||||
|
|
@ -44,12 +39,9 @@ export const AuthRoot = observer(function AuthRoot(props: TAuthRoot) {
|
|||
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
||||
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
||||
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
|
||||
|
||||
// hooks
|
||||
const { config } = useInstance();
|
||||
|
||||
// derived values
|
||||
const isOAuthEnabled = isOAuthEnabledHelper(config);
|
||||
const oAuthActionText = authMode === EAuthModes.SIGN_UP ? "Sign up" : "Sign in";
|
||||
const { isOAuthEnabled, oAuthOptions } = useOAuthConfig(oAuthActionText);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authMode && currentAuthMode) setAuthMode(currentAuthMode);
|
||||
|
|
@ -99,15 +91,11 @@ export const AuthRoot = observer(function AuthRoot(props: TAuthRoot) {
|
|||
|
||||
if (!authMode) return <></>;
|
||||
|
||||
const OauthButtonContent = authMode === EAuthModes.SIGN_UP ? "Sign up" : "Sign in";
|
||||
|
||||
const OAuthConfig = OAUTH_CONFIG({ OauthButtonContent, next_path, config, resolvedTheme });
|
||||
|
||||
return (
|
||||
<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">
|
||||
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
|
||||
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
|
||||
<AuthBanner message={errorInfo.message} handleBannerData={(value) => setErrorInfo(value)} />
|
||||
)}
|
||||
<AuthHeader
|
||||
workspaceSlug={workspaceSlug?.toString() || undefined}
|
||||
|
|
@ -117,7 +105,7 @@ export const AuthRoot = observer(function AuthRoot(props: TAuthRoot) {
|
|||
currentAuthStep={authStep}
|
||||
/>
|
||||
|
||||
{isOAuthEnabled && <OAuthOptions options={OAuthConfig} compact={authStep === EAuthSteps.PASSWORD} />}
|
||||
{isOAuthEnabled && <OAuthOptions options={oAuthOptions} compact={authStep === EAuthSteps.PASSWORD} />}
|
||||
|
||||
<AuthFormRoot
|
||||
authStep={authStep}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export const ResetPasswordForm = observer(function ResetPasswordForm() {
|
|||
<AuthFormHeader title="Reset password" description="Create a new password." />
|
||||
|
||||
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
|
||||
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
|
||||
<AuthBanner message={errorInfo.message} handleBannerData={(value) => setErrorInfo(value)} />
|
||||
)}
|
||||
<form
|
||||
className="space-y-4"
|
||||
|
|
|
|||
|
|
@ -33,14 +33,12 @@ export const AuthHeader = observer(function AuthHeader({ type }: AuthHeaderProps
|
|||
const { config } = useInstance();
|
||||
// derived values
|
||||
const enableSignUpConfig = config?.enable_signup ?? false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={t(authContentMap[type].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>
|
||||
{enableSignUpConfig && (
|
||||
<AuthHeaderBase
|
||||
pageTitle={t(authContentMap[type].pageTitle)}
|
||||
additionalAction={
|
||||
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">
|
||||
<span className="text-body-sm-regular text-tertiary">{t(authContentMap[type].text)}</span>
|
||||
<Link
|
||||
|
|
@ -51,8 +49,28 @@ export const AuthHeader = observer(function AuthHeader({ type }: AuthHeaderProps
|
|||
{t(authContentMap[type].linkText)}
|
||||
</Link>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
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);
|
||||
|
||||
setYjsSession({ provider, ydoc: provider.document as Y.Doc });
|
||||
setYjsSession({ provider, ydoc: provider.document });
|
||||
|
||||
// Handle page visibility changes (sleep/wake, tab switching)
|
||||
const handleVisibilityChange = (event?: Event) => {
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
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 = {
|
||||
key: string;
|
||||
key: TInstanceAuthenticationModeKeys;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
|
|
@ -53,4 +67,17 @@ export type TGetBaseAuthenticationModeProps = {
|
|||
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";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue