Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 546198f3b6 | |||
| fa1b4a102a | |||
| 8399f64bee | |||
| 5f3f9d2623 | |||
| 110dbd9acd | |||
| b83d460938 | |||
| ea1f92e0c6 | |||
| 3d5e427894 | |||
| 6129e8e53b | |||
| d61b157929 | |||
| a37e5e2f6a | |||
| a976bea5af | |||
| 1072509642 | |||
| dcd8d27eae | |||
| c97e418515 | |||
| 031baaa162 | |||
| 9141a9377f | |||
| 866338289e | |||
| 8d479ac24c | |||
| 94d5779f3a | |||
| 20492ffa75 | |||
| 630d2b9600 | |||
| ec215bb3b1 | |||
| 3241693d6d | |||
| 6cc5c1946a | |||
| 2980c2d76b | |||
| 92ac28fcb8 | |||
| e3ba7c2889 | |||
| 78eeb9ce0d | |||
| f04be48f61 | |||
| 5499e49b72 | |||
| 3c8624b1ba | |||
| 55c49cc633 | |||
| 5b28327551 | |||
| 39728d4cc4 | |||
| 59f26a80bb | |||
| 1a7c537ee5 | |||
| 27a7cdcfc3 | |||
| 0c795e95ac | |||
| 777200db7b | |||
| d09c91b838 | |||
| 2bc7080d24 | |||
| 6cd85a7095 | |||
| 5590bf7198 | |||
| 70eea50db5 | |||
| 373e640a25 | |||
| 21df1028f8 | |||
| c0552f06f2 | |||
| dbf84bf6bb | |||
| cb9b4f7887 | |||
| 7a3cebdb6b | |||
| 5a2e2e7e11 | |||
| e2fcb5044d | |||
| daf65f5ddd | |||
| 8a0b710a62 | |||
| 72883e76a5 | |||
| 57bbf3cf43 | |||
| 46453560f2 | |||
| c2ce21e56c | |||
| 4908211fe6 | |||
| 83496487d4 | |||
| 409a3e84ab | |||
| 313314ebd6 | |||
| 1ad5772694 | |||
| 67c39dfc3d | |||
| 3876bf054c | |||
| df69886080 | |||
| f3d5e7def3 | |||
| 1e3514575f | |||
| 465c99f742 | |||
| 81dbd5ab19 | |||
| cb56fbe3ca | |||
| 80acecb77a | |||
| 5e0091e32e | |||
| 59f024dad3 | |||
| ba7b2a3e27 | |||
| eafa393524 | |||
| 9747bffbe2 | |||
| b165e2a3fe | |||
| 3df58397b5 | |||
| e56417e056 | |||
| cae66ef853 | |||
| 67416935b4 | |||
| 78f4a7294b | |||
| ad2291dfee | |||
| c56bb06957 | |||
| 88f4d8253d | |||
| 9b90fbf5aa | |||
| d9ab60104e | |||
| f0b98ceb8f | |||
| 4cac953cd1 | |||
| 508e332a23 | |||
| b906d42e1d | |||
| 8b0a8160a1 | |||
| b7621c62eb | |||
| 617c7ab734 | |||
| b675c5c7a7 | |||
| 2f80d59ba9 | |||
| 8bd23ac44b | |||
| dbda7504b2 | |||
| 61db42bcb7 | |||
| 1f06b67c66 | |||
| 1338e9d2a4 | |||
| 7c6a6a3432 | |||
| d8930c5188 | |||
| 88f5a063d6 | |||
| df710e00dc | |||
| e8047b6f1c | |||
| fa63964566 | |||
| be1113b170 | |||
| e7974afd8b | |||
| fc1c7fe235 | |||
| 22339b9786 | |||
| d86418aad8 | |||
| 0263f07977 | |||
| 2ac5efe2f0 | |||
| 1b427392c4 | |||
| 192ed9ba8d | |||
| 119cf39b4d | |||
| 234b088c4a | |||
| bc37edbe9a | |||
| 7451d5e125 | |||
| 58e48a995b |
@@ -105,10 +105,8 @@ CLAUDE.md
|
||||
|
||||
build/
|
||||
.react-router/
|
||||
AGENTS.md
|
||||
|
||||
build/
|
||||
.react-router/
|
||||
AGENTS.md
|
||||
temp/
|
||||
scripts/
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Agent Development Guide
|
||||
|
||||
## Commands
|
||||
|
||||
- `pnpm dev` - Start all dev servers (web:3000, admin:3001)
|
||||
- `pnpm build` - Build all packages and apps
|
||||
- `pnpm check` - Run all checks (format, lint, types)
|
||||
- `pnpm check:lint` - ESLint across all packages
|
||||
- `pnpm check:types` - TypeScript type checking
|
||||
- `pnpm fix` - Auto-fix format and lint issues
|
||||
- `pnpm turbo run <command> --filter=<package>` - Target specific package/app
|
||||
- `pnpm --filter=@plane/ui storybook` - Start Storybook on port 6006
|
||||
|
||||
## Code Style
|
||||
|
||||
- **Imports**: Use `workspace:*` for internal packages, `catalog:` for external deps
|
||||
- **TypeScript**: Strict mode enabled, all files must be typed
|
||||
- **Formatting**: Prettier with Tailwind plugin, run `pnpm fix:format`
|
||||
- **Linting**: ESLint with shared config, max warnings vary by package
|
||||
- **Naming**: camelCase for variables/functions, PascalCase for components/types
|
||||
- **Error Handling**: Use try-catch with proper error types, log errors appropriately
|
||||
- **State Management**: MobX stores in `packages/shared-state`, reactive patterns
|
||||
- **Testing**: All features require unit tests, use existing test framework per package
|
||||
- **Components**: Build in `@plane/ui` with Storybook for isolated development
|
||||
@@ -54,7 +54,7 @@ Getting started with Plane is simple. Choose the setup that works best for you:
|
||||
|
||||
## 🌟 Features
|
||||
|
||||
- **Issues**
|
||||
- **Work Items**
|
||||
Efficiently create and manage tasks with a robust rich text editor that supports file uploads. Enhance organization and tracking by adding sub-properties and referencing related issues.
|
||||
|
||||
- **Cycles**
|
||||
@@ -72,15 +72,13 @@ Getting started with Plane is simple. Choose the setup that works best for you:
|
||||
- **Analytics**
|
||||
Access real-time insights across all your Plane data. Visualize trends, remove blockers, and keep your projects moving forward.
|
||||
|
||||
- **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution.
|
||||
|
||||
## 🛠️ Local development
|
||||
|
||||
See [CONTRIBUTING](./CONTRIBUTING.md)
|
||||
|
||||
## ⚙️ Built with
|
||||
|
||||
[](https://nextjs.org/)
|
||||
[](https://reactrouter.com/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://nodejs.org/en)
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||
<a
|
||||
href="https://platform.openai.com/docs/models/overview"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
@@ -63,7 +63,7 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||
<a
|
||||
href="https://platform.openai.com/api-keys"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
@@ -94,8 +94,8 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="pb-1 text-xl font-medium text-custom-text-100">OpenAI</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">If you use ChatGPT, this is for you.</div>
|
||||
<div className="pb-1 text-18 font-medium text-primary">OpenAI</div>
|
||||
<div className="text-13 font-regular text-tertiary">If you use ChatGPT, this is for you.</div>
|
||||
</div>
|
||||
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-x-12 gap-y-8 lg:grid-cols-3">
|
||||
{aiFormFields.map((field) => (
|
||||
@@ -114,13 +114,13 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
<div className="flex flex-col gap-4 items-start">
|
||||
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</Button>
|
||||
|
||||
<div className="relative inline-flex items-center gap-2 rounded border border-custom-primary-100/20 bg-custom-primary-100/10 px-4 py-2 text-xs text-custom-primary-200">
|
||||
<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-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">AI features for all your workspaces</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,15 +4,16 @@ import Link from "next/link";
|
||||
import { useForm } from "react-hook-form";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IFormattedInstanceConfiguration, TInstanceGiteaAuthenticationConfigurationKeys } from "@plane/types";
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { CodeBlock } from "@/components/common/code-block";
|
||||
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
import type { TControllerSwitchFormField } from "@/components/common/controller-switch";
|
||||
import { ControllerSwitch } from "@/components/common/controller-switch";
|
||||
import type { TCopyField } from "@/components/common/copy-field";
|
||||
import { CopyField } from "@/components/common/copy-field";
|
||||
// hooks
|
||||
@@ -41,6 +42,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
GITEA_HOST: config["GITEA_HOST"] || "https://gitea.com",
|
||||
GITEA_CLIENT_ID: config["GITEA_CLIENT_ID"],
|
||||
GITEA_CLIENT_SECRET: config["GITEA_CLIENT_SECRET"],
|
||||
ENABLE_GITEA_SYNC: config["ENABLE_GITEA_SYNC"] || "0",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,7 +71,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://gitea.com/user/settings/applications"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Gitea OAuth application settings.
|
||||
@@ -91,7 +93,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://gitea.com/user/settings/applications"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Gitea OAuth application settings.
|
||||
@@ -104,6 +106,11 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
},
|
||||
];
|
||||
|
||||
const GITEA_FORM_SWITCH_FIELD: TControllerSwitchFormField<GiteaConfigFormValues> = {
|
||||
name: "ENABLE_GITEA_SYNC",
|
||||
label: "Gitea",
|
||||
};
|
||||
|
||||
const GITEA_SERVICE_FIELD: TCopyField[] = [
|
||||
{
|
||||
key: "Callback_URI",
|
||||
@@ -117,7 +124,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href={`${control._formValues.GITEA_HOST || "https://gitea.com"}/user/settings/applications`}
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
@@ -130,20 +137,22 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
const onSubmit = async (formData: GiteaConfigFormValues) => {
|
||||
const payload: Partial<GiteaConfigFormValues> = { ...formData };
|
||||
|
||||
await updateInstanceConfigurations(payload)
|
||||
.then((response = []) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Done!",
|
||||
message: "Your Gitea authentication is configured. You should test it now.",
|
||||
});
|
||||
reset({
|
||||
GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value,
|
||||
GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value,
|
||||
GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value,
|
||||
});
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
try {
|
||||
const response = await updateInstanceConfigurations(payload);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Done!",
|
||||
message: "Your Gitea authentication is configured. You should test it now.",
|
||||
});
|
||||
reset({
|
||||
GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value,
|
||||
GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value,
|
||||
GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value,
|
||||
ENABLE_GITEA_SYNC: response.find((item) => item.key === "ENABLE_GITEA_SYNC")?.value,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
@@ -163,7 +172,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
|
||||
<div className="pt-2.5 text-xl font-medium">Gitea-provided details for Plane</div>
|
||||
<div className="pt-2.5 text-18 font-medium">Gitea-provided details for Plane</div>
|
||||
{GITEA_FORM_FIELDS.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
@@ -177,24 +186,27 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||
required={field.required}
|
||||
/>
|
||||
))}
|
||||
<ControllerSwitch control={control} field={GITEA_FORM_SWITCH_FIELD} />
|
||||
<div className="flex flex-col gap-1 pt-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
<Link
|
||||
href="/authentication"
|
||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
||||
onClick={handleGoBack}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={(e) => void handleSubmit(onSubmit)(e)}
|
||||
loading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</Button>
|
||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
</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-custom-background-80/60 rounded-lg">
|
||||
<div className="pt-2 text-xl font-medium">Plane-provided details for Gitea</div>
|
||||
<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-custom-border-100 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" }];
|
||||
|
||||
@@ -8,12 +8,12 @@ import { API_BASE_URL } from "@plane/constants";
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IFormattedInstanceConfiguration, TInstanceGithubAuthenticationConfigurationKeys } from "@plane/types";
|
||||
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { CodeBlock } from "@/components/common/code-block";
|
||||
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import type { TControllerSwitchFormField } from "@/components/common/controller-switch";
|
||||
import { ControllerSwitch } from "@/components/common/controller-switch";
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
import type { TCopyField } from "@/components/common/copy-field";
|
||||
import { CopyField } from "@/components/common/copy-field";
|
||||
@@ -43,6 +43,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
GITHUB_CLIENT_ID: config["GITHUB_CLIENT_ID"],
|
||||
GITHUB_CLIENT_SECRET: config["GITHUB_CLIENT_SECRET"],
|
||||
GITHUB_ORGANIZATION_ID: config["GITHUB_ORGANIZATION_ID"],
|
||||
ENABLE_GITHUB_SYNC: config["ENABLE_GITHUB_SYNC"] || "0",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -60,7 +61,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub OAuth application settings.
|
||||
@@ -82,7 +83,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub OAuth application settings.
|
||||
@@ -104,6 +105,11 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
},
|
||||
];
|
||||
|
||||
const GITHUB_FORM_SWITCH_FIELD: TControllerSwitchFormField<GithubConfigFormValues> = {
|
||||
name: "ENABLE_GITHUB_SYNC",
|
||||
label: "GitHub",
|
||||
};
|
||||
|
||||
const GITHUB_COMMON_SERVICE_DETAILS: TCopyField[] = [
|
||||
{
|
||||
key: "Origin_URL",
|
||||
@@ -116,7 +122,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
@@ -139,7 +145,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://github.com/settings/applications/new"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
@@ -152,20 +158,22 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
const onSubmit = async (formData: GithubConfigFormValues) => {
|
||||
const payload: Partial<GithubConfigFormValues> = { ...formData };
|
||||
|
||||
await updateInstanceConfigurations(payload)
|
||||
.then((response = []) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Done!",
|
||||
message: "Your GitHub authentication is configured. You should test it now.",
|
||||
});
|
||||
reset({
|
||||
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
|
||||
GITHUB_CLIENT_SECRET: response.find((item) => item.key === "GITHUB_CLIENT_SECRET")?.value,
|
||||
GITHUB_ORGANIZATION_ID: response.find((item) => item.key === "GITHUB_ORGANIZATION_ID")?.value,
|
||||
});
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
try {
|
||||
const response = await updateInstanceConfigurations(payload);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Done!",
|
||||
message: "Your GitHub authentication is configured. You should test it now.",
|
||||
});
|
||||
reset({
|
||||
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
|
||||
GITHUB_CLIENT_SECRET: response.find((item) => item.key === "GITHUB_CLIENT_SECRET")?.value,
|
||||
GITHUB_ORGANIZATION_ID: response.find((item) => item.key === "GITHUB_ORGANIZATION_ID")?.value,
|
||||
ENABLE_GITHUB_SYNC: response.find((item) => item.key === "ENABLE_GITHUB_SYNC")?.value,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
@@ -185,7 +193,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
|
||||
<div className="pt-2.5 text-xl font-medium">GitHub-provided details for Plane</div>
|
||||
<div className="pt-2.5 text-18 font-medium">GitHub-provided details for Plane</div>
|
||||
{GITHUB_FORM_FIELDS.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
@@ -199,27 +207,30 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
required={field.required}
|
||||
/>
|
||||
))}
|
||||
<ControllerSwitch control={control} field={GITHUB_FORM_SWITCH_FIELD} />
|
||||
<div className="flex flex-col gap-1 pt-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
<Link
|
||||
href="/authentication"
|
||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
||||
onClick={handleGoBack}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={(e) => void handleSubmit(onSubmit)(e)}
|
||||
loading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</Button>
|
||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
|
||||
<div className="pt-2 text-xl font-medium">Plane-provided details for GitHub</div>
|
||||
<div className="pt-2 text-18 font-medium">Plane-provided details for GitHub</div>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
{/* common service details */}
|
||||
<div className="flex flex-col gap-y-4 px-6 py-4 bg-custom-background-80 rounded-lg">
|
||||
<div className="flex flex-col gap-y-4 px-6 py-4 bg-layer-1 rounded-lg">
|
||||
{GITHUB_COMMON_SERVICE_DETAILS.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
@@ -227,11 +238,11 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||
|
||||
{/* web service details */}
|
||||
<div className="flex flex-col rounded-lg overflow-hidden">
|
||||
<div className="px-6 py-3 bg-custom-background-80/60 font-medium text-xs uppercase flex items-center gap-x-3 text-custom-text-200">
|
||||
<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>
|
||||
<div className="px-6 py-4 flex flex-col gap-y-4 bg-custom-background-80">
|
||||
<div className="px-6 py-4 flex flex-col gap-y-4 bg-layer-1">
|
||||
{GITHUB_SERVICE_DETAILS.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
|
||||
@@ -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-custom-border-100 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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -7,11 +7,12 @@ import { API_BASE_URL } from "@plane/constants";
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigurationKeys } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { CodeBlock } from "@/components/common/code-block";
|
||||
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import type { TControllerSwitchFormField } from "@/components/common/controller-switch";
|
||||
import { ControllerSwitch } from "@/components/common/controller-switch";
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
import type { TCopyField } from "@/components/common/copy-field";
|
||||
import { CopyField } from "@/components/common/copy-field";
|
||||
@@ -41,6 +42,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
GITLAB_HOST: config["GITLAB_HOST"],
|
||||
GITLAB_CLIENT_ID: config["GITLAB_CLIENT_ID"],
|
||||
GITLAB_CLIENT_SECRET: config["GITLAB_CLIENT_SECRET"],
|
||||
ENABLE_GITLAB_SYNC: config["ENABLE_GITLAB_SYNC"] || "0",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -71,7 +73,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitLab OAuth application settings
|
||||
@@ -94,7 +96,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitLab OAuth application settings
|
||||
@@ -108,6 +110,11 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
},
|
||||
];
|
||||
|
||||
const GITLAB_FORM_SWITCH_FIELD: TControllerSwitchFormField<GitlabConfigFormValues> = {
|
||||
name: "ENABLE_GITLAB_SYNC",
|
||||
label: "GitLab",
|
||||
};
|
||||
|
||||
const GITLAB_SERVICE_FIELD: TCopyField[] = [
|
||||
{
|
||||
key: "Callback_URL",
|
||||
@@ -120,7 +127,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitLab OAuth application
|
||||
@@ -134,20 +141,22 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
const onSubmit = async (formData: GitlabConfigFormValues) => {
|
||||
const payload: Partial<GitlabConfigFormValues> = { ...formData };
|
||||
|
||||
await updateInstanceConfigurations(payload)
|
||||
.then((response = []) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Done!",
|
||||
message: "Your GitLab authentication is configured. You should test it now.",
|
||||
});
|
||||
reset({
|
||||
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value,
|
||||
GITLAB_CLIENT_ID: response.find((item) => item.key === "GITLAB_CLIENT_ID")?.value,
|
||||
GITLAB_CLIENT_SECRET: response.find((item) => item.key === "GITLAB_CLIENT_SECRET")?.value,
|
||||
});
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
try {
|
||||
const response = await updateInstanceConfigurations(payload);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Done!",
|
||||
message: "Your GitLab authentication is configured. You should test it now.",
|
||||
});
|
||||
reset({
|
||||
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value,
|
||||
GITLAB_CLIENT_ID: response.find((item) => item.key === "GITLAB_CLIENT_ID")?.value,
|
||||
GITLAB_CLIENT_SECRET: response.find((item) => item.key === "GITLAB_CLIENT_SECRET")?.value,
|
||||
ENABLE_GITLAB_SYNC: response.find((item) => item.key === "ENABLE_GITLAB_SYNC")?.value,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
@@ -167,7 +176,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
|
||||
<div className="pt-2.5 text-xl font-medium">GitLab-provided details for Plane</div>
|
||||
<div className="pt-2.5 text-18 font-medium">GitLab-provided details for Plane</div>
|
||||
{GITLAB_FORM_FIELDS.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
@@ -181,24 +190,27 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||
required={field.required}
|
||||
/>
|
||||
))}
|
||||
<ControllerSwitch control={control} field={GITLAB_FORM_SWITCH_FIELD} />
|
||||
<div className="flex flex-col gap-1 pt-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
<Link
|
||||
href="/authentication"
|
||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
||||
onClick={handleGoBack}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={(e) => void handleSubmit(onSubmit)(e)}
|
||||
loading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</Button>
|
||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
</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-custom-background-80/60 rounded-lg">
|
||||
<div className="pt-2 text-xl font-medium">Plane-provided details for GitLab</div>
|
||||
<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-custom-border-100 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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -8,11 +8,12 @@ import { API_BASE_URL } from "@plane/constants";
|
||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { IFormattedInstanceConfiguration, TInstanceGoogleAuthenticationConfigurationKeys } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { CodeBlock } from "@/components/common/code-block";
|
||||
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||
import type { TControllerSwitchFormField } from "@/components/common/controller-switch";
|
||||
import { ControllerSwitch } from "@/components/common/controller-switch";
|
||||
import { ControllerInput } from "@/components/common/controller-input";
|
||||
import type { TCopyField } from "@/components/common/copy-field";
|
||||
import { CopyField } from "@/components/common/copy-field";
|
||||
@@ -41,6 +42,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
defaultValues: {
|
||||
GOOGLE_CLIENT_ID: config["GOOGLE_CLIENT_ID"],
|
||||
GOOGLE_CLIENT_SECRET: config["GOOGLE_CLIENT_SECRET"],
|
||||
ENABLE_GOOGLE_SYNC: config["ENABLE_GOOGLE_SYNC"] || "0",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -58,7 +60,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
@@ -80,7 +82,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
tabIndex={-1}
|
||||
href="https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
@@ -93,6 +95,11 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
},
|
||||
];
|
||||
|
||||
const GOOGLE_FORM_SWITCH_FIELD: TControllerSwitchFormField<GoogleConfigFormValues> = {
|
||||
name: "ENABLE_GOOGLE_SYNC",
|
||||
label: "Google",
|
||||
};
|
||||
|
||||
const GOOGLE_COMMON_SERVICE_DETAILS: TCopyField[] = [
|
||||
{
|
||||
key: "Origin_URL",
|
||||
@@ -105,7 +112,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
<a
|
||||
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
@@ -127,7 +134,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
<a
|
||||
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here.
|
||||
@@ -140,19 +147,21 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
const onSubmit = async (formData: GoogleConfigFormValues) => {
|
||||
const payload: Partial<GoogleConfigFormValues> = { ...formData };
|
||||
|
||||
await updateInstanceConfigurations(payload)
|
||||
.then((response = []) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Done!",
|
||||
message: "Your Google authentication is configured. You should test it now.",
|
||||
});
|
||||
reset({
|
||||
GOOGLE_CLIENT_ID: response.find((item) => item.key === "GOOGLE_CLIENT_ID")?.value,
|
||||
GOOGLE_CLIENT_SECRET: response.find((item) => item.key === "GOOGLE_CLIENT_SECRET")?.value,
|
||||
});
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
try {
|
||||
const response = await updateInstanceConfigurations(payload);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Done!",
|
||||
message: "Your Google authentication is configured. You should test it now.",
|
||||
});
|
||||
reset({
|
||||
GOOGLE_CLIENT_ID: response.find((item) => item.key === "GOOGLE_CLIENT_ID")?.value,
|
||||
GOOGLE_CLIENT_SECRET: response.find((item) => item.key === "GOOGLE_CLIENT_SECRET")?.value,
|
||||
ENABLE_GOOGLE_SYNC: response.find((item) => item.key === "ENABLE_GOOGLE_SYNC")?.value,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
@@ -172,7 +181,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
|
||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
|
||||
<div className="pt-2.5 text-xl font-medium">Google-provided details for Plane</div>
|
||||
<div className="pt-2.5 text-18 font-medium">Google-provided details for Plane</div>
|
||||
{GOOGLE_FORM_FIELDS.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
@@ -186,27 +195,30 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
required={field.required}
|
||||
/>
|
||||
))}
|
||||
<ControllerSwitch control={control} field={GOOGLE_FORM_SWITCH_FIELD} />
|
||||
<div className="flex flex-col gap-1 pt-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
<Link
|
||||
href="/authentication"
|
||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
||||
onClick={handleGoBack}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={(e) => void handleSubmit(onSubmit)(e)}
|
||||
loading={isSubmitting}
|
||||
disabled={!isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</Button>
|
||||
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
|
||||
<div className="pt-2 text-xl font-medium">Plane-provided details for Google</div>
|
||||
<div className="pt-2 text-18 font-medium">Plane-provided details for Google</div>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
{/* common service details */}
|
||||
<div className="flex flex-col gap-y-4 px-6 py-4 bg-custom-background-80 rounded-lg">
|
||||
<div className="flex flex-col gap-y-4 px-6 py-4 bg-layer-1 rounded-lg">
|
||||
{GOOGLE_COMMON_SERVICE_DETAILS.map((field) => (
|
||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||
))}
|
||||
@@ -214,11 +226,11 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||
|
||||
{/* web service details */}
|
||||
<div className="flex flex-col rounded-lg overflow-hidden">
|
||||
<div className="px-6 py-3 bg-custom-background-80/60 font-medium text-xs uppercase flex items-center gap-x-3 text-custom-text-200">
|
||||
<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>
|
||||
<div className="px-6 py-4 flex flex-col gap-y-4 bg-custom-background-80">
|
||||
<div className="px-6 py-4 flex flex-col gap-y-4 bg-layer-1">
|
||||
{GOOGLE_SERVICE_DETAILS.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 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-custom-border-100 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-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Manage authentication modes for your instance</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
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")}>
|
||||
<div className="flex grow items-center gap-4">
|
||||
<div className="grow">
|
||||
<div className="text-lg font-medium pb-1">Allow anyone to sign up even without an invite</div>
|
||||
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
|
||||
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-lg 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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -157,12 +157,12 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||
/>
|
||||
))}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">Email security</h4>
|
||||
<h4 className="text-13 text-tertiary">Email security</h4>
|
||||
<CustomSelect
|
||||
value={emailSecurityKey}
|
||||
label={EMAIL_SECURITY_OPTIONS[emailSecurityKey]}
|
||||
onChange={handleEmailSecurityChange}
|
||||
buttonClassName="rounded-md border-custom-border-200"
|
||||
buttonClassName="rounded-md border-subtle"
|
||||
input
|
||||
>
|
||||
{Object.entries(EMAIL_SECURITY_OPTIONS).map(([key, value]) => (
|
||||
@@ -173,12 +173,12 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||
</CustomSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-custom-border-100">
|
||||
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-subtle">
|
||||
<div className="flex w-full max-w-xl flex-col gap-y-10 px-1">
|
||||
<div className="mr-8 flex items-center gap-10 pt-4">
|
||||
<div className="grow">
|
||||
<div className="text-sm font-medium text-custom-text-100">Authentication</div>
|
||||
<div className="text-xs font-normal text-custom-text-300">
|
||||
<div className="text-13 font-medium text-primary">Authentication</div>
|
||||
<div className="text-11 font-regular text-tertiary">
|
||||
This is optional, but we recommend setting up a username and a password for your SMTP server.
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,14 +204,16 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||
<div className="flex max-w-4xl items-center py-1 gap-4">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
loading={isSubmitting}
|
||||
disabled={!isValid || !isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
{isSubmitting ? "Saving" : "Save changes"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
onClick={() => setIsSendTestEmailModalOpen(true)}
|
||||
loading={isSubmitting}
|
||||
disabled={!isValid}
|
||||
|
||||
@@ -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-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Set it up below and please test your settings before you save them.
|
||||
<span className="text-red-400">Misconfigs can lead to email bounces and errors.</span>
|
||||
</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">
|
||||
Set it up below and please test your settings before you save them.
|
||||
<span className="text-danger-primary">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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ export function SendTestEmailModal(props: Props) {
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
<div className="fixed inset-0 bg-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="my-10 flex justify-center p-4 text-center sm:p-0 md:my-20">
|
||||
@@ -85,8 +85,8 @@ export function SendTestEmailModal(props: Props) {
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 px-4 text-left shadow-custom-shadow-md transition-all w-full sm:max-w-xl">
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-surface-1 p-5 px-4 text-left shadow-raised-200 transition-all w-full sm:max-w-xl">
|
||||
<h3 className="text-16 font-medium leading-6 text-primary">
|
||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL
|
||||
? "Send test email"
|
||||
: sendEmailStep === ESendEmailSteps.SUCCESS
|
||||
@@ -101,12 +101,12 @@ export function SendTestEmailModal(props: Props) {
|
||||
value={receiverEmail}
|
||||
onChange={(e) => setReceiverEmail(e.target.value)}
|
||||
placeholder="Receiver email"
|
||||
className="w-full resize-none text-lg"
|
||||
className="w-full resize-none text-16"
|
||||
tabIndex={1}
|
||||
/>
|
||||
)}
|
||||
{sendEmailStep === ESendEmailSteps.SUCCESS && (
|
||||
<div className="flex flex-col gap-y-4 text-sm">
|
||||
<div className="flex flex-col gap-y-4 text-13">
|
||||
<p>
|
||||
We have sent the test email to {receiverEmail}. Please check your spam folder if you cannot find
|
||||
it.
|
||||
@@ -114,14 +114,14 @@ export function SendTestEmailModal(props: Props) {
|
||||
<p>If you still cannot find it, recheck your SMTP configuration and trigger a new test email.</p>
|
||||
</div>
|
||||
)}
|
||||
{sendEmailStep === ESendEmailSteps.FAILED && <div className="text-sm">{error}</div>}
|
||||
{sendEmailStep === ESendEmailSteps.FAILED && <div className="text-13">{error}</div>}
|
||||
<div className="flex items-center gap-2 justify-end mt-5">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={2}>
|
||||
<Button variant="secondary" size="lg" onClick={handleClose} tabIndex={2}>
|
||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL ? "Cancel" : "Close"}
|
||||
</Button>
|
||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
|
||||
<Button variant="primary" size="sm" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
|
||||
{isLoading ? "Sending email..." : "Send email"}
|
||||
<Button variant="primary" size="lg" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
|
||||
{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,
|
||||
@@ -63,8 +63,8 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-3">
|
||||
<div className="text-lg font-medium">Instance details</div>
|
||||
<div className="space-y-4">
|
||||
<div className="text-16 font-medium text-primary">Instance details</div>
|
||||
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
<ControllerInput
|
||||
key="instance_name"
|
||||
@@ -78,54 +78,52 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">Email</h4>
|
||||
<h4 className="text-13 text-tertiary">Email</h4>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={instanceAdmins[0]?.user_detail?.email ?? ""}
|
||||
placeholder="Admin email"
|
||||
className="w-full cursor-not-allowed !text-custom-text-400"
|
||||
className="w-full cursor-not-allowed !text-placeholder"
|
||||
autoComplete="on"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">Instance ID</h4>
|
||||
<h4 className="text-13 text-tertiary">Instance ID</h4>
|
||||
<Input
|
||||
id="instance_id"
|
||||
name="instance_id"
|
||||
type="text"
|
||||
value={instance.instance_id}
|
||||
className="w-full cursor-not-allowed rounded-md font-medium !text-custom-text-400"
|
||||
className="w-full cursor-not-allowed rounded-md font-medium !text-placeholder"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="text-lg font-medium">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-custom-border-200 rounded">
|
||||
<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-custom-background-80 rounded-full">
|
||||
<Telescope className="w-6 h-6 text-custom-text-300/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">
|
||||
<div className="text-sm font-medium text-custom-text-100 leading-5">
|
||||
Let Plane collect anonymous usage data
|
||||
</div>
|
||||
<div className="text-xs font-normal text-custom-text-300 leading-5">
|
||||
<div className="text-13 font-medium text-primary leading-5">Let Plane collect anonymous usage data</div>
|
||||
<div className="text-11 font-regular text-tertiary leading-5">
|
||||
No PII is collected.This anonymized data is used to understand how you use Plane and build new features
|
||||
in line with{" "}
|
||||
<a
|
||||
href="https://developers.plane.so/self-hosting/telemetry"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
our Telemetry Policy.
|
||||
@@ -146,8 +144,15 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button variant="primary" 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,22 +44,22 @@ 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-custom-border-200 rounded">
|
||||
<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-custom-background-80 rounded-full">
|
||||
<MessageSquare className="w-6 h-6 text-custom-text-300/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>
|
||||
|
||||
<div className="grow">
|
||||
<div className="text-sm font-medium text-custom-text-100 leading-5">Chat with us</div>
|
||||
<div className="text-xs font-normal text-custom-text-300 leading-5">
|
||||
<div className="text-13 font-medium text-primary leading-5">Chat with us</div>
|
||||
<div className="text-11 font-regular text-tertiary leading-5">
|
||||
Let your users chat with us via Intercom or another service. Toggling Telemetry off turns this off
|
||||
automatically.
|
||||
</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-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">General settings</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
|
||||
<a
|
||||
href="https://unsplash.com/documentation#creating-a-developer-account"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
className="text-accent-primary hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more.
|
||||
@@ -70,8 +70,8 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
{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-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Third-party image libraries</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
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) {
|
||||
@@ -34,9 +34,9 @@ function AdminLayout(_props: Route.ComponentProps) {
|
||||
return (
|
||||
<div className="relative flex h-screen w-screen overflow-hidden">
|
||||
<AdminSidebar />
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-surface-1">
|
||||
<AdminHeader />
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<div className="h-full w-full overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -33,20 +33,20 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||
const getSidebarMenuItems = () => (
|
||||
<Menu.Items
|
||||
className={cn(
|
||||
"absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y divide-custom-sidebar-border-100 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none",
|
||||
"absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y divide-subtle rounded-md border border-subtle bg-surface-1 px-1 py-2 text-11 shadow-lg outline-none",
|
||||
{
|
||||
"left-4": isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-2.5 pb-2">
|
||||
<span className="px-2 text-custom-sidebar-text-200 truncate">{currentUser?.email}</span>
|
||||
<span className="px-2 text-secondary truncate">{currentUser?.email}</span>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
className="flex w-full items-center gap-2 rounded-sm px-2 py-1 hover:bg-layer-1-hover"
|
||||
onClick={handleThemeSwitch}
|
||||
>
|
||||
<Palette className="h-4 w-4 stroke-[1.5]" />
|
||||
@@ -59,7 +59,7 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||
<Menu.Item
|
||||
as="button"
|
||||
type="submit"
|
||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||
className="flex w-full items-center gap-2 rounded-sm px-2 py-1 hover:bg-layer-1-hover"
|
||||
>
|
||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||
Sign out
|
||||
@@ -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-custom-sidebar-border-200 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 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 bg-custom-sidebar-background-80">
|
||||
<UserCog2 className="h-5 w-5 text-custom-text-200" />
|
||||
<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-base font-medium text-custom-text-200">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-base"
|
||||
className="!text-body-sm-medium"
|
||||
/>
|
||||
</Menu.Button>
|
||||
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { ExternalLink, HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
// plane internal packages
|
||||
import { WEB_BASE_URL } from "@plane/constants";
|
||||
import { DiscordIcon, GithubIcon, PageIcon } from "@plane/propel/icons";
|
||||
// plane internal packages
|
||||
import { DiscordIcon, GithubIcon, NewTabIcon, 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);
|
||||
@@ -45,7 +44,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 h-14 flex-shrink-0",
|
||||
"flex w-full items-center justify-between gap-1 self-baseline border-t border-subtle bg-surface-1 px-4 h-14 flex-shrink-0",
|
||||
{
|
||||
"flex-col h-auto py-1.5": isSidebarCollapsed,
|
||||
}
|
||||
@@ -55,32 +54,32 @@ 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 border border-custom-primary-100/20 bg-custom-primary-100/10 text-xs text-custom-primary-200 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} />
|
||||
<NewTabIcon width={14} height={14} />
|
||||
{!isSidebarCollapsed && "Redirect to Plane"}
|
||||
</a>
|
||||
</Tooltip>
|
||||
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
||||
<button
|
||||
type="button"
|
||||
className={`ml-auto grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
className={`ml-auto grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
|
||||
isSidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
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">
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
className={`grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
|
||||
isSidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
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>
|
||||
@@ -98,7 +97,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||
<div
|
||||
className={`absolute bottom-2 min-w-[10rem] z-[15] ${
|
||||
isSidebarCollapsed ? "left-full" : "-left-[75px]"
|
||||
} divide-y divide-custom-border-200 whitespace-nowrap rounded bg-custom-background-100 p-1 shadow-custom-shadow-xs`}
|
||||
} divide-y divide-subtle-1 whitespace-nowrap rounded-sm bg-surface-1 p-1 shadow-raised-100`}
|
||||
ref={helpOptionsRef}
|
||||
>
|
||||
<div className="space-y-1 pb-2">
|
||||
@@ -106,11 +105,11 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||
if (href)
|
||||
return (
|
||||
<Link href={href} key={name} target="_blank">
|
||||
<div className="flex items-center gap-x-2 rounded px-2 py-1 text-xs hover:bg-custom-background-80">
|
||||
<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-custom-text-200" width={14} height={14} />
|
||||
<Icon className="h-3.5 w-3.5 text-secondary" />
|
||||
</div>
|
||||
<span className="text-xs">{name}</span>
|
||||
<span className="text-11">{name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
@@ -119,17 +118,17 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
className="flex w-full items-center gap-x-2 rounded px-2 py-1 text-xs hover:bg-custom-background-80"
|
||||
className="flex w-full items-center gap-x-2 rounded-sm px-2 py-1 text-11 hover:bg-layer-1"
|
||||
>
|
||||
<div className="grid flex-shrink-0 place-items-center">
|
||||
<Icon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
<Icon className="h-3.5 w-3.5 text-secondary" />
|
||||
</div>
|
||||
<span className="text-xs">{name}</span>
|
||||
<span className="text-11">{name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="px-2 pb-1 pt-2 text-[10px]">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-custom-primary-100/10 text-custom-primary-100"
|
||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80",
|
||||
"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-sm font-medium transition-colors`,
|
||||
isActive ? "text-custom-primary-100" : "text-custom-sidebar-text-200"
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
`text-[10px] transition-colors`,
|
||||
isActive ? "text-custom-primary-90" : "text-custom-sidebar-text-400"
|
||||
)}
|
||||
>
|
||||
{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>
|
||||
|
||||
@@ -38,7 +38,7 @@ export const AdminSidebar = observer(function AdminSidebar() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300
|
||||
className={`inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-subtle bg-surface-1 duration-300
|
||||
fixed md:relative
|
||||
${isSidebarCollapsed ? "-ml-[290px]" : ""}
|
||||
sm:${isSidebarCollapsed ? "-ml-[290px]" : ""}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function WorkspaceCreateForm() {
|
||||
<div className="space-y-8">
|
||||
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-start justify-between gap-x-10 gap-y-6 lg:grid-cols-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">Name your workspace</h4>
|
||||
<h4 className="text-13 text-tertiary">Name your workspace</h4>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -118,13 +118,13 @@ export function WorkspaceCreateForm() {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs text-red-500">{errors?.name?.message}</span>
|
||||
<span className="text-11 text-danger-primary">{errors?.name?.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">Set your workspace's URL</h4>
|
||||
<div className="flex gap-0.5 w-full items-center rounded-md border-[0.5px] border-custom-border-200 px-3">
|
||||
<span className="whitespace-nowrap text-sm text-custom-text-200">{workspaceBaseURL}</span>
|
||||
<h4 className="text-13 text-tertiary">Set your workspace's URL</h4>
|
||||
<div className="flex gap-0.5 w-full items-center rounded-md border-[0.5px] border-subtle px-3">
|
||||
<span className="whitespace-nowrap text-13 text-secondary">{workspaceBaseURL}</span>
|
||||
<Controller
|
||||
control={control}
|
||||
name="slug"
|
||||
@@ -148,19 +148,19 @@ export function WorkspaceCreateForm() {
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.slug)}
|
||||
placeholder="workspace-name"
|
||||
className="block w-full rounded-md border-none bg-transparent !px-0 py-2 text-sm"
|
||||
className="block w-full rounded-md border-none bg-transparent !px-0 py-2 text-13"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{slugError && <p className="text-sm text-red-500">This URL is taken. Try something else.</p>}
|
||||
{slugError && <p className="text-13 text-danger-primary">This URL is taken. Try something else.</p>}
|
||||
{invalidSlug && (
|
||||
<p className="text-sm text-red-500">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
|
||||
<p className="text-13 text-danger-primary">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
|
||||
)}
|
||||
{errors.slug && <span className="text-xs text-red-500">{errors.slug.message}</span>}
|
||||
{errors.slug && <span className="text-11 text-danger-primary">{errors.slug.message}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">How many people will use this workspace?</h4>
|
||||
<h4 className="text-13 text-tertiary">How many people will use this workspace?</h4>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
name="organization_size"
|
||||
@@ -172,10 +172,10 @@ export function WorkspaceCreateForm() {
|
||||
onChange={onChange}
|
||||
label={
|
||||
ORGANIZATION_SIZE.find((c) => c === value) ?? (
|
||||
<span className="text-custom-text-400">Select a range</span>
|
||||
<span className="text-placeholder">Select a range</span>
|
||||
)
|
||||
}
|
||||
buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none"
|
||||
buttonClassName="!border-[0.5px] !border-subtle !shadow-none"
|
||||
input
|
||||
>
|
||||
{ORGANIZATION_SIZE.map((item) => (
|
||||
@@ -187,7 +187,7 @@ export function WorkspaceCreateForm() {
|
||||
)}
|
||||
/>
|
||||
{errors.organization_size && (
|
||||
<span className="text-sm text-red-500">{errors.organization_size.message}</span>
|
||||
<span className="text-13 text-danger-primary">{errors.organization_size.message}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,14 +195,14 @@ export function WorkspaceCreateForm() {
|
||||
<div className="flex max-w-4xl items-center py-1 gap-4">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
size="lg"
|
||||
onClick={handleSubmit(handleCreateWorkspace)}
|
||||
disabled={!isValid}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Creating workspace" : "Create workspace"}
|
||||
</Button>
|
||||
<Link className={getButtonStyling("neutral-primary", "sm")} href="/workspace">
|
||||
<Link className={getButtonStyling("secondary", "lg")} href="/workspace">
|
||||
Go back
|
||||
</Link>
|
||||
</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-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Create a new workspace on this instance.</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
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,101 +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-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-xl font-medium text-custom-text-100">Workspaces on this instance</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
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")}>
|
||||
<div className="flex grow items-center gap-4">
|
||||
<div className="grow">
|
||||
<div className="text-lg font-medium pb-1">Prevent anyone else from creating a workspace.</div>
|
||||
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
|
||||
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-lg font-medium">
|
||||
All workspaces on this instance{" "}
|
||||
<span className="text-custom-text-300">• {workspaceIds.length}</span>
|
||||
{workspaceLoader && ["mutation", "pagination"].includes(workspaceLoader) && (
|
||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
|
||||
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", "sm")}>
|
||||
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-primary"
|
||||
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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ export function AuthBanner(props: TAuthBanner) {
|
||||
|
||||
if (!bannerData) return <></>;
|
||||
return (
|
||||
<div className="relative flex items-center p-2 rounded-md gap-2 border border-custom-primary-100/50 bg-custom-primary-100/10">
|
||||
<div className="relative flex items-center p-2 rounded-md gap-2 border border-accent-strong/50 bg-accent-primary/10">
|
||||
<div className="w-4 h-4 flex-shrink-0 relative flex justify-center items-center">
|
||||
<Info size={16} className="text-custom-primary-100" />
|
||||
<Info size={16} className="text-accent-primary" />
|
||||
</div>
|
||||
<div className="w-full text-sm font-medium text-custom-primary-100">{bannerData?.message}</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-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/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" />
|
||||
|
||||
@@ -5,7 +5,7 @@ export function AuthHeader() {
|
||||
return (
|
||||
<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-custom-text-100" />
|
||||
<PlaneLockup height={20} width={95} className="text-primary" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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-custom-text-300/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-custom-text-300/80" />,
|
||||
icon: <KeyRound className="h-6 w-6 p-0.5 text-tertiary" />,
|
||||
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ function RootLayout() {
|
||||
}, [replace, isUserLoggedIn]);
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8">
|
||||
<div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8 bg-surface-1">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -128,11 +128,11 @@ export function InstanceSignInForm() {
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
|
||||
Email <span className="text-red-500">*</span>
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="email">
|
||||
Email <span className="text-danger-primary">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
@@ -146,12 +146,12 @@ export function InstanceSignInForm() {
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
|
||||
Password <span className="text-red-500">*</span>
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="password">
|
||||
Password <span className="text-danger-primary">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
@@ -164,7 +164,7 @@ export function InstanceSignInForm() {
|
||||
{showPassword ? (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
|
||||
onClick={() => setShowPassword(false)}
|
||||
>
|
||||
<EyeOff className="h-4 w-4" />
|
||||
@@ -172,7 +172,7 @@ export function InstanceSignInForm() {
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
|
||||
onClick={() => setShowPassword(true)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
@@ -181,7 +181,7 @@ export function InstanceSignInForm() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
|
||||
<Button type="submit" size="xl" className="w-full" disabled={isButtonDisabled}>
|
||||
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Sign in"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<svg width="92" height="84" viewBox="0 0 92 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3695_11896)">
|
||||
<path d="M83.0398 32.6876C74.2901 27.2397 62.0735 23.8553 48.7013 23.8553C21.7918 23.8553 0 37.3101 0 53.9016C0 69.0898 18.1598 81.554 41.685 83.7001V74.9504C25.8364 72.9693 13.95 64.3022 13.95 53.9016C13.95 42.0977 29.4684 32.44 48.7013 32.44C58.2765 32.44 66.9436 34.8338 73.217 38.7134L64.3022 44.2439H92.1197V27.0746L83.0398 32.6876Z" fill="#CCCCCC"/>
|
||||
<path d="M41.6846 8.99736V74.9504V83.7002L55.6346 74.9504V0L41.6846 8.99736Z" fill="#FF6200"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3695_11896">
|
||||
<rect width="92" height="84" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 702 B |
@@ -1,17 +0,0 @@
|
||||
<svg width="700" height="650" viewBox="0 0 700 650" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3262_5767)">
|
||||
<mask id="mask0_3262_5767" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="700" height="650">
|
||||
<path d="M700 0H0V650H700V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_3262_5767)">
|
||||
<path d="M337.682 0L360.832 20.5C377.982 35.7 395.182 50.85 412.132 66.25C521.982 166 614.982 278.25 684.982 407.45C688.582 414.05 691.832 420.85 695.082 427.6L699.982 437.75L694.582 440.6L690.532 434.85L680.032 419.9L672.682 409.2C621.732 335.25 570.682 261.2 500.582 201.95C479.373 183.995 455.969 168.807 430.932 156.75C380.232 132.5 335.132 142.2 296.432 182C259.632 219.85 240.532 266.85 223.282 314.65C221.032 320.8 218.682 326.9 216.332 333L212.232 343.75L203.632 341C208.632 323.6 213.232 306.1 217.832 288.55C228.332 248.8 238.832 209.05 253.432 170.75C268.932 129.95 288.532 90.6 308.082 51.25C316.532 34.2 324.982 17.15 333.082 0H337.682Z" fill="#C22E33"/>
|
||||
<path d="M372.382 491.1C291.082 529.6 94.3829 569.3 1.08287 559.1C-14.1671 478.8 135.482 102.5 208.982 45.5L204.232 56.4C202.115 61.531 199.813 66.5842 197.332 71.55L194.032 78C156.032 151.1 118.082 224.3 98.6329 304.5C91.6287 332.124 87.8038 360.458 87.2328 388.95C86.7328 455.95 128.432 501.55 198.082 504.4C231.582 505.75 265.432 502.25 299.232 498.7C313.932 497.2 328.582 495.65 343.232 494.5C348.632 494.1 353.932 493.45 360.832 492.55L372.382 491.15V491.1Z" fill="#C22E33"/>
|
||||
<path d="M141.233 639.05C118.983 640.75 96.733 642.45 74.583 644.45C279.433 663.95 476.083 630.6 670.083 562.25C606.833 450.75 521.583 362.7 422.483 286.15C423.783 291.05 426.683 294.6 429.533 298.1L431.933 301.1C440.433 312.4 449.333 323.5 458.283 334.6C478.733 360.05 499.183 385.5 514.583 413.5C553.483 484.5 532.383 545.9 456.183 578.3C406.083 599.65 351.333 614.2 297.183 622.9C245.683 631.1 193.433 635.05 141.233 639.05Z" fill="#C22E33"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3262_5767">
|
||||
<rect width="700" height="650" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -7,22 +7,22 @@ import Image404 from "@/app/assets/images/404.svg?url";
|
||||
|
||||
function PageNotFound() {
|
||||
return (
|
||||
<div className={`h-screen w-full overflow-hidden bg-custom-background-100`}>
|
||||
<div className={`h-screen w-full overflow-hidden bg-surface-1`}>
|
||||
<div className="grid h-full place-items-center p-4">
|
||||
<div className="space-y-8 text-center">
|
||||
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
|
||||
<img src={Image404} alt="404 - Page not found" className="h-full w-full object-contain" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Oops! Something went wrong.</h3>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
<h3 className="text-16 font-semibold">Oops! Something went wrong.</h3>
|
||||
<p className="text-13 text-secondary">
|
||||
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
|
||||
temporarily unavailable.
|
||||
</p>
|
||||
</div>
|
||||
<Link to="/general/">
|
||||
<span className="flex justify-center py-4">
|
||||
<Button variant="neutral-primary" size="md">
|
||||
<Button variant="secondary" size="lg">
|
||||
Go to general settings
|
||||
</Button>
|
||||
</span>
|
||||
|
||||
+19
-3
@@ -1,15 +1,20 @@
|
||||
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";
|
||||
import "@fontsource/material-symbols-rounded";
|
||||
import "@fontsource/ibm-plex-mono";
|
||||
|
||||
const APP_TITLE = "Plane | Simple, extensible, open-source project management tool.";
|
||||
const APP_DESCRIPTION =
|
||||
@@ -22,6 +27,13 @@ export const links: LinksFunction = () => [
|
||||
{ rel: "shortcut icon", href: faviconIco },
|
||||
{ rel: "manifest", href: `/site.webmanifest.json` },
|
||||
{ rel: "stylesheet", href: globalStyles },
|
||||
{
|
||||
rel: "preload",
|
||||
href: interVariableWoff2,
|
||||
as: "font",
|
||||
type: "font/woff2",
|
||||
crossOrigin: "anonymous",
|
||||
},
|
||||
];
|
||||
|
||||
export function Layout({ children }: { children: ReactNode }) {
|
||||
@@ -56,7 +68,11 @@ export const meta: Route.MetaFunction = () => [
|
||||
];
|
||||
|
||||
export default function Root() {
|
||||
return <Outlet />;
|
||||
return (
|
||||
<div className="bg-canvas min-h-screen">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HydrateFallback() {
|
||||
|
||||
@@ -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", "sm"))}
|
||||
rel="noreferrer"
|
||||
>
|
||||
Upgrade
|
||||
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -16,8 +16,8 @@ export function AuthenticationMethodCard(props: Props) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("w-full flex items-center gap-14 rounded", {
|
||||
"px-4 py-3 border border-custom-border-200": withBorder,
|
||||
className={cn("w-full flex items-center gap-14 rounded-lg bg-layer-2", {
|
||||
"px-4 py-3 border border-subtle": withBorder,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
@@ -26,21 +26,21 @@ export function AuthenticationMethodCard(props: Props) {
|
||||
})}
|
||||
>
|
||||
<div className="shrink-0">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-custom-background-80">{icon}</div>
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-layer-1">{icon}</div>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div
|
||||
className={cn("font-medium leading-5 text-custom-text-100", {
|
||||
"text-sm": withBorder,
|
||||
"text-xl": !withBorder,
|
||||
className={cn("font-medium leading-5 text-primary", {
|
||||
"text-13": withBorder,
|
||||
"text-18": !withBorder,
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div
|
||||
className={cn("font-normal leading-5 text-custom-text-300", {
|
||||
"text-xs": withBorder,
|
||||
"text-sm": !withBorder,
|
||||
className={cn("font-regular leading-5 text-tertiary", {
|
||||
"text-11": withBorder,
|
||||
"text-13": !withBorder,
|
||||
})}
|
||||
>
|
||||
{description}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
// icons
|
||||
import { Settings2 } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
@@ -28,7 +28,7 @@ export const GiteaConfiguration = observer(function GiteaConfiguration(props: Pr
|
||||
<>
|
||||
{GiteaConfigured ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/authentication/gitea" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
|
||||
<Link href="/authentication/gitea" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
@@ -43,11 +43,8 @@ export const GiteaConfiguration = observer(function GiteaConfiguration(props: Pr
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href="/authentication/gitea"
|
||||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
||||
>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
||||
<Link href="/authentication/gitea" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||
<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
|
||||
@@ -28,7 +27,7 @@ export const GithubConfiguration = observer(function GithubConfiguration(props:
|
||||
<>
|
||||
{isGithubConfigured ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/authentication/github" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
|
||||
<Link href="/authentication/github" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
@@ -42,11 +41,8 @@ export const GithubConfiguration = observer(function GithubConfiguration(props:
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href="/authentication/github"
|
||||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
||||
>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
||||
<Link href="/authentication/github" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
||||
Configure
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const GitlabConfiguration = observer(function GitlabConfiguration(props:
|
||||
<>
|
||||
{isGitlabConfigured ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
|
||||
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
@@ -41,11 +41,8 @@ export const GitlabConfiguration = observer(function GitlabConfiguration(props:
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href="/authentication/gitlab"
|
||||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
||||
>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
||||
<Link href="/authentication/gitlab" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
||||
Configure
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const GoogleConfiguration = observer(function GoogleConfiguration(props:
|
||||
<>
|
||||
{isGoogleConfigured ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/authentication/google" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
|
||||
<Link href="/authentication/google" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||
Edit
|
||||
</Link>
|
||||
<ToggleSwitch
|
||||
@@ -41,11 +41,8 @@ export const GoogleConfiguration = observer(function GoogleConfiguration(props:
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href="/authentication/google"
|
||||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
||||
>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
||||
<Link href="/authentication/google" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
||||
Configure
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -10,20 +10,22 @@ export function Banner(props: TBanner) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-md p-2 w-full border ${type === "error" ? "bg-red-500/5 border-red-400" : "bg-green-500/5 border-green-400"}`}
|
||||
className={`rounded-md p-2 w-full border ${type === "error" ? "bg-danger-subtle border-danger-strong" : "bg-success-subtle border-success-strong"}`}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex-shrink-0">
|
||||
{type === "error" ? (
|
||||
<span className="flex items-center justify-center h-6 w-6 rounded-full">
|
||||
<AlertCircle className="h-5 w-5 text-red-600" aria-hidden="true" />
|
||||
<AlertCircle className="h-5 w-5 text-danger-primary" aria-hidden="true" />
|
||||
</span>
|
||||
) : (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600" aria-hidden="true" />
|
||||
<CheckCircle2 className="h-5 w-5 text-success-primary" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-1">
|
||||
<p className={`text-sm font-medium ${type === "error" ? "text-red-600" : "text-green-600"}`}>{message}</p>
|
||||
<p className={`text-13 font-medium ${type === "error" ? "text-danger-primary" : "text-success-primary"}`}>
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,17 +14,12 @@ export function BreadcrumbLink(props: Props) {
|
||||
<li className="flex items-center space-x-2" tabIndex={-1}>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
{href ? (
|
||||
<Link
|
||||
className="flex items-center gap-1 text-sm font-medium text-custom-text-300 hover:text-custom-text-100"
|
||||
href={href}
|
||||
>
|
||||
{icon && (
|
||||
<div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-[1rem]">{icon}</div>
|
||||
)}
|
||||
<Link className="flex items-center gap-1 text-13 font-medium text-tertiary hover:text-primary" href={href}>
|
||||
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-16">{icon}</div>}
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="flex cursor-default items-center gap-1 text-sm font-medium text-custom-text-100">
|
||||
<div className="flex cursor-default items-center gap-1 text-13 font-medium text-primary">
|
||||
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden">{icon}</div>}
|
||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
||||
</div>
|
||||
|
||||
@@ -10,9 +10,9 @@ export function CodeBlock({ children, className, darkerShade }: TProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"px-0.5 text-xs text-custom-text-300 bg-custom-background-90 font-semibold rounded-md border border-custom-border-100",
|
||||
"px-0.5 text-11 text-tertiary bg-surface-2 font-semibold rounded-md border border-subtle",
|
||||
{
|
||||
"text-custom-text-200 bg-custom-background-80 border-custom-border-200": darkerShade,
|
||||
"text-secondary bg-layer-1 border-subtle": darkerShade,
|
||||
},
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function ConfirmDiscardModal(props: Props) {
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
<div className="fixed inset-0 bg-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-32">
|
||||
@@ -39,15 +39,15 @@ export function ConfirmDiscardModal(props: Props) {
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[30rem]">
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[30rem]">
|
||||
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-300">
|
||||
<Dialog.Title as="h3" className="text-16 font-medium leading-6 text-tertiary">
|
||||
You have unsaved changes
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-custom-text-400">
|
||||
<p className="text-13 text-placeholder">
|
||||
Changes you made will be lost if you go back. Do you wish to go back?
|
||||
</p>
|
||||
</div>
|
||||
@@ -55,10 +55,10 @@ export function ConfirmDiscardModal(props: Props) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end items-center p-4 sm:px-6 gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
<Button variant="secondary" size="lg" onClick={handleClose}>
|
||||
Keep editing
|
||||
</Button>
|
||||
<Link href={onDiscardHref} className={getButtonStyling("primary", "sm")}>
|
||||
<Link href={onDiscardHref} className={getButtonStyling("primary", "base")}>
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ export function ControllerInput(props: Props) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">{label}</h4>
|
||||
<h4 className="text-13 text-tertiary">{label}</h4>
|
||||
<div className="relative">
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -61,7 +61,7 @@ export function ControllerInput(props: Props) {
|
||||
(showPassword ? (
|
||||
<button
|
||||
tabIndex={-1}
|
||||
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
|
||||
className="absolute right-3 top-2.5 flex items-center justify-center text-placeholder"
|
||||
onClick={() => setShowPassword(false)}
|
||||
>
|
||||
<EyeOff className="h-4 w-4" />
|
||||
@@ -69,14 +69,14 @@ export function ControllerInput(props: Props) {
|
||||
) : (
|
||||
<button
|
||||
tabIndex={-1}
|
||||
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
|
||||
className="absolute right-3 top-2.5 flex items-center justify-center text-placeholder"
|
||||
onClick={() => setShowPassword(true)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{description && <p className="pt-0.5 text-xs text-custom-text-300">{description}</p>}
|
||||
{description && <p className="pt-0.5 text-11 text-tertiary">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Control, FieldPath, FieldValues } from "react-hook-form";
|
||||
import { Controller } from "react-hook-form";
|
||||
// plane internal packages
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
|
||||
type Props<T extends FieldValues = FieldValues> = {
|
||||
control: Control<T>;
|
||||
field: TControllerSwitchFormField<T>;
|
||||
};
|
||||
|
||||
export type TControllerSwitchFormField<T extends FieldValues = FieldValues> = {
|
||||
name: FieldPath<T>;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export function ControllerSwitch<T extends FieldValues>(props: Props<T>) {
|
||||
const {
|
||||
control,
|
||||
field: { name, label },
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">Refresh user attributes from {label} during sign in</h4>
|
||||
<div className="relative">
|
||||
<Controller
|
||||
control={control}
|
||||
name={name as FieldPath<T>}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const parsedValue = Number.parseInt(typeof value === "string" ? value : String(value ?? "0"), 10);
|
||||
const isOn = !Number.isNaN(parsedValue) && parsedValue !== 0;
|
||||
return <ToggleSwitch value={isOn} onChange={() => onChange(isOn ? "0" : "1")} size="sm" />;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
// ui
|
||||
import { Copy } from "lucide-react";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { CopyIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
|
||||
type Props = {
|
||||
@@ -22,9 +22,10 @@ export function CopyField(props: Props) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-200">{label}</h4>
|
||||
<h4 className="text-13 text-secondary">{label}</h4>
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="flex items-center justify-between py-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(url);
|
||||
@@ -35,10 +36,10 @@ export function CopyField(props: Props) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<p className="text-sm font-medium">{url}</p>
|
||||
<Copy size={18} color="#B9B9B9" />
|
||||
<p className="text-13 font-medium">{url}</p>
|
||||
<CopyIcon width={18} height={18} color="#B9B9B9" />
|
||||
</Button>
|
||||
<div className="text-xs text-custom-text-300">{description}</div>
|
||||
<div className="text-11 text-tertiary">{description}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ export function EmptyState({ title, description, image, primaryButton, secondary
|
||||
<div className={`flex h-full w-full items-center justify-center`}>
|
||||
<div className="flex w-full flex-col items-center text-center">
|
||||
{image && <img src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
|
||||
<h6 className="mb-3 mt-6 text-xl font-semibold sm:mt-8">{title}</h6>
|
||||
{description && <p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>}
|
||||
<h6 className="mb-3 mt-6 text-18 font-semibold sm:mt-8">{title}</h6>
|
||||
{description && <p className="mb-7 px-5 text-tertiary sm:mb-8">{description}</p>}
|
||||
<div className="flex items-center gap-4">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
@@ -28,6 +28,7 @@ export function EmptyState({ title, description, image, primaryButton, secondary
|
||||
prependIcon={primaryButton.icon}
|
||||
onClick={primaryButton.onClick}
|
||||
disabled={disabled}
|
||||
size="lg"
|
||||
>
|
||||
{primaryButton.text}
|
||||
</Button>
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const EXTENDED_HEADER_SEGMENT_LABELS: Record<string, string> = {};
|
||||
+16
-37
@@ -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 flex justify-center items-center bg-custom-background-80 transition-all hover:bg-custom-background-90 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-custom-text-200 group-hover:text-custom-text-100 transition-all" />
|
||||
</div>
|
||||
<Menu size={14} className="text-secondary group-hover:text-primary transition-all" />
|
||||
</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,17 +40,17 @@ 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-custom-sidebar-border-200 bg-custom-sidebar-background-100 p-4">
|
||||
<div className="relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-subtle bg-surface-1 p-4">
|
||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||
<HamburgerToggle />
|
||||
{breadcrumbItems.length >= 0 && (
|
||||
@@ -82,7 +61,7 @@ export const AdminHeader = observer(function AdminHeader() {
|
||||
<BreadcrumbLink
|
||||
href="/general/"
|
||||
label="Settings"
|
||||
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
||||
icon={<Settings className="h-4 w-4 text-tertiary" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -22,13 +22,13 @@ export const InstanceFailureView = observer(function InstanceFailureView() {
|
||||
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<img src={instanceImage} alt="Instance failure illustration" />
|
||||
<h3 className="font-medium text-2xl text-white text-center">Unable to fetch instance details.</h3>
|
||||
<p className="font-medium text-base text-center">
|
||||
<h3 className="font-medium text-20 text-on-color text-center">Unable to fetch instance details.</h3>
|
||||
<p className="font-medium text-14 text-center">
|
||||
We were unable to fetch the details of the instance. Fret not, it might just be a connectivity issue.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button size="md" onClick={handleRetry}>
|
||||
<Button size="lg" onClick={handleRetry}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export function FormHeader({ heading, subHeading }: { heading: string; subHeading: string }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-2xl font-semibold text-custom-text-100 leading-7">{heading}</span>
|
||||
<span className="text-lg font-semibold text-custom-text-400 leading-7">{subHeading}</span>
|
||||
<span className="text-20 font-semibold text-primary leading-7">{heading}</span>
|
||||
<span className="text-16 font-semibold text-placeholder leading-7">{subHeading}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,16 +8,14 @@ export function InstanceNotReady() {
|
||||
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
|
||||
<div className="w-auto max-w-2xl relative space-y-8 py-10">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
|
||||
<h1 className="text-24 font-bold pb-3">Welcome aboard Plane!</h1>
|
||||
<img src={PlaneTakeOffImage} alt="Plane Logo" />
|
||||
<p className="font-medium text-base text-custom-text-400">
|
||||
Get started by setting up your instance and workspace
|
||||
</p>
|
||||
<p className="font-medium text-14 text-placeholder">Get started by setting up your instance and workspace</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Link href={"/setup/?auth_enabled=0"}>
|
||||
<Button size="lg" className="w-full">
|
||||
<Button size="xl" className="w-full">
|
||||
Get started
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -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,
|
||||
@@ -156,11 +156,11 @@ export function InstanceSetupForm() {
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center gap-4">
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="first_name">
|
||||
First name <span className="text-red-500">*</span>
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="first_name">
|
||||
First name <span className="text-danger-primary">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
id="first_name"
|
||||
name="first_name"
|
||||
type="text"
|
||||
@@ -173,11 +173,11 @@ export function InstanceSetupForm() {
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="last_name">
|
||||
Last name <span className="text-red-500">*</span>
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="last_name">
|
||||
Last name <span className="text-danger-primary">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
id="last_name"
|
||||
name="last_name"
|
||||
type="text"
|
||||
@@ -191,11 +191,11 @@ export function InstanceSetupForm() {
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
|
||||
Email <span className="text-red-500">*</span>
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="email">
|
||||
Email <span className="text-danger-primary">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
@@ -207,16 +207,16 @@ export function InstanceSetupForm() {
|
||||
autoComplete="on"
|
||||
/>
|
||||
{errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (
|
||||
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
|
||||
<p className="px-1 text-11 text-danger-primary">{errorData.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="company_name">
|
||||
Company name <span className="text-red-500">*</span>
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="company_name">
|
||||
Company name <span className="text-danger-primary">*</span>
|
||||
</label>
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
id="company_name"
|
||||
name="company_name"
|
||||
type="text"
|
||||
@@ -228,17 +228,17 @@ export function InstanceSetupForm() {
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
|
||||
Set a password <span className="text-red-500">*</span>
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="password">
|
||||
Set a password <span className="text-danger-primary">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||
id="password"
|
||||
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}
|
||||
@@ -250,7 +250,7 @@ export function InstanceSetupForm() {
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
|
||||
onClick={() => handleShowPassword("password")}
|
||||
>
|
||||
<EyeOff className="h-4 w-4" />
|
||||
@@ -259,7 +259,7 @@ export function InstanceSetupForm() {
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
|
||||
onClick={() => handleShowPassword("password")}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
@@ -267,14 +267,14 @@ export function InstanceSetupForm() {
|
||||
)}
|
||||
</div>
|
||||
{errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && (
|
||||
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
|
||||
<p className="px-1 text-11 text-danger-primary">{errorData.message}</p>
|
||||
)}
|
||||
<PasswordStrengthIndicator password={formData.password} isFocused={isPasswordInputFocused} />
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="confirm_password">
|
||||
Confirm password <span className="text-red-500">*</span>
|
||||
<label className="text-13 text-tertiary font-medium" htmlFor="confirm_password">
|
||||
Confirm password <span className="text-danger-primary">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
@@ -285,7 +285,7 @@ export function InstanceSetupForm() {
|
||||
value={formData.confirm_password}
|
||||
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
|
||||
placeholder="Confirm password"
|
||||
className="w-full border border-custom-border-100 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
|
||||
className="w-full border border-subtle !bg-surface-1 pr-12 placeholder:text-placeholder"
|
||||
onFocus={() => setIsRetryPasswordInputFocused(true)}
|
||||
onBlur={() => setIsRetryPasswordInputFocused(false)}
|
||||
/>
|
||||
@@ -293,7 +293,7 @@ export function InstanceSetupForm() {
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
|
||||
onClick={() => handleShowPassword("retypePassword")}
|
||||
>
|
||||
<EyeOff className="h-4 w-4" />
|
||||
@@ -302,7 +302,7 @@ export function InstanceSetupForm() {
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
||||
className="absolute right-3 top-3.5 flex items-center justify-center text-placeholder"
|
||||
onClick={() => handleShowPassword("retypePassword")}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
@@ -311,7 +311,9 @@ export function InstanceSetupForm() {
|
||||
</div>
|
||||
{!!formData.confirm_password &&
|
||||
formData.password !== formData.confirm_password &&
|
||||
renderPasswordMatchError && <span className="text-sm text-red-500">Passwords don{"'"}t match</span>}
|
||||
renderPasswordMatchError && (
|
||||
<span className="text-13 text-danger-primary">Passwords don{"'"}t match</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative flex gap-2">
|
||||
@@ -324,14 +326,14 @@ export function InstanceSetupForm() {
|
||||
checked={formData.is_telemetry_enabled}
|
||||
/>
|
||||
</div>
|
||||
<label className="text-sm text-custom-text-300 font-medium cursor-pointer" htmlFor="is_telemetry_enabled">
|
||||
<label className="text-13 text-tertiary font-medium cursor-pointer" htmlFor="is_telemetry_enabled">
|
||||
Allow Plane to anonymously collect usage events.{" "}
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href="https://developers.plane.so/self-hosting/telemetry"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm font-medium text-blue-500 hover:text-blue-600 flex-shrink-0"
|
||||
className="text-13 font-medium text-blue-500 hover:text-blue-600 flex-shrink-0"
|
||||
>
|
||||
See More
|
||||
</a>
|
||||
@@ -339,7 +341,7 @@ export function InstanceSetupForm() {
|
||||
</div>
|
||||
|
||||
<div className="py-2">
|
||||
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
|
||||
<Button type="submit" size="xl" className="w-full" disabled={isButtonDisabled}>
|
||||
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -18,19 +18,19 @@ export const NewUserPopup = observer(function NewUserPopup() {
|
||||
|
||||
if (!isNewUserPopup) return <></>;
|
||||
return (
|
||||
<div className="absolute bottom-8 right-8 p-6 w-96 border border-custom-border-100 shadow-md rounded-lg bg-custom-background-100">
|
||||
<div className="absolute bottom-8 right-8 p-6 w-96 border border-subtle shadow-md rounded-lg bg-surface-1">
|
||||
<div className="flex gap-4">
|
||||
<div className="grow">
|
||||
<div className="text-base font-semibold">Create workspace</div>
|
||||
<div className="py-2 text-sm font-medium text-custom-text-300">
|
||||
<div className="text-14 font-semibold">Create workspace</div>
|
||||
<div className="py-2 text-13 font-medium text-tertiary">
|
||||
Instance setup done! Welcome to Plane instance portal. Start your journey with by creating your first
|
||||
workspace.
|
||||
</div>
|
||||
<div className="flex items-center gap-4 pt-2">
|
||||
<Link href="/workspace/create" className={getButtonStyling("primary", "sm")}>
|
||||
<Link href="/workspace/create" className={getButtonStyling("primary", "lg")}>
|
||||
Create workspace
|
||||
</Link>
|
||||
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
|
||||
<Button variant="secondary" size="lg" onClick={toggleNewUserPopup}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
// plane internal packages
|
||||
import { WEB_BASE_URL } from "@plane/constants";
|
||||
import { NewTabIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// hooks
|
||||
@@ -23,19 +24,19 @@ 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-custom-border-200/70 hover:border-custom-border-200 hover:bg-custom-background-90 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-xs uppercase ${
|
||||
!workspace?.logo_url && "rounded bg-custom-primary-500 text-white"
|
||||
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-lg bg-accent-primary text-on-color"
|
||||
}`}
|
||||
>
|
||||
{workspace?.logo_url && workspace.logo_url !== "" ? (
|
||||
<img
|
||||
src={getFileURL(workspace.logo_url)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded object-cover"
|
||||
className="absolute left-0 top-0 h-full w-full rounded-sm object-cover"
|
||||
alt="Workspace Logo"
|
||||
/>
|
||||
) : (
|
||||
@@ -44,30 +45,30 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
|
||||
</span>
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<div className="flex flex-wrap w-full items-center gap-2.5">
|
||||
<h3 className={`text-base font-medium capitalize`}>{workspace.name}</h3>/
|
||||
<h3 className={`text-14 font-medium capitalize`}>{workspace.name}</h3>/
|
||||
<Tooltip tooltipContent="The unique URL of your workspace">
|
||||
<h4 className="text-sm text-custom-text-300">[{workspace.slug}]</h4>
|
||||
<h4 className="text-13 text-tertiary">[{workspace.slug}]</h4>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{workspace.owner.email && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<h3 className="text-custom-text-200 font-medium">Owned by:</h3>
|
||||
<h4 className="text-custom-text-300">{workspace.owner.email}</h4>
|
||||
<div className="flex items-center gap-1 text-11">
|
||||
<h3 className="text-secondary font-medium">Owned by:</h3>
|
||||
<h4 className="text-tertiary">{workspace.owner.email}</h4>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
<div className="flex items-center gap-2.5 text-11">
|
||||
{workspace.total_projects !== null && (
|
||||
<span className="flex items-center gap-1">
|
||||
<h3 className="text-custom-text-200 font-medium">Total projects:</h3>
|
||||
<h4 className="text-custom-text-300">{workspace.total_projects}</h4>
|
||||
<h3 className="text-secondary font-medium">Total projects:</h3>
|
||||
<h4 className="text-tertiary">{workspace.total_projects}</h4>
|
||||
</span>
|
||||
)}
|
||||
{workspace.total_members !== null && (
|
||||
<>
|
||||
•
|
||||
<span className="flex items-center gap-1">
|
||||
<h3 className="text-custom-text-200 font-medium">Total members:</h3>
|
||||
<h4 className="text-custom-text-300">{workspace.total_members}</h4>
|
||||
<h3 className="text-secondary font-medium">Total members:</h3>
|
||||
<h4 className="text-tertiary">{workspace.total_members}</h4>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
@@ -75,7 +76,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<ExternalLink size={14} className="text-custom-text-400 group-hover:text-custom-text-200" />
|
||||
<NewTabIcon width={14} height={16} className="text-placeholder group-hover:text-secondary" />
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
|
||||
+20
-69
@@ -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-custom-text-300/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-custom-text-300/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 (
|
||||
<>
|
||||
{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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 => {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Image, BrainCog, Cog, Mail } from "lucide-react";
|
||||
// plane imports
|
||||
import { LockIcon, 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: LockIcon,
|
||||
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/`,
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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 />
|
||||
@@ -0,0 +1,3 @@
|
||||
export function ExtendedProviders({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -20,6 +20,12 @@ http {
|
||||
server {
|
||||
listen 3000;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@bprogress/core": "catalog:",
|
||||
"@fontsource-variable/inter": "5.2.8",
|
||||
"@fontsource/ibm-plex-mono": "5.2.7",
|
||||
"@fontsource/material-symbols-rounded": "5.2.30",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@plane/constants": "workspace:*",
|
||||
"@plane/hooks": "workspace:*",
|
||||
@@ -37,12 +40,11 @@
|
||||
"lucide-react": "catalog:",
|
||||
"mobx": "catalog:",
|
||||
"mobx-react": "catalog:",
|
||||
"next-themes": "^0.2.1",
|
||||
"next-themes": "0.4.6",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-hook-form": "7.51.5",
|
||||
"react-router": "catalog:",
|
||||
"react-router-dom": "catalog:",
|
||||
"serve": "14.2.5",
|
||||
"swr": "catalog:",
|
||||
"uuid": "catalog:"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require("@plane/tailwind-config/postcss.config.js");
|
||||
@@ -0,0 +1,3 @@
|
||||
import postcssConfig from "@plane/tailwind-config/postcss.config.js";
|
||||
|
||||
export default postcssConfig;
|
||||
@@ -1,373 +1,4 @@
|
||||
@import "@plane/propel/styles/fonts.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.text-1\.5xl {
|
||||
font-size: 1.375rem;
|
||||
line-height: 1.875rem;
|
||||
}
|
||||
|
||||
.text-2\.5xl {
|
||||
font-size: 1.75rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light !important;
|
||||
|
||||
--color-primary-10: 229, 243, 250;
|
||||
--color-primary-20: 216, 237, 248;
|
||||
--color-primary-30: 199, 229, 244;
|
||||
--color-primary-40: 169, 214, 239;
|
||||
--color-primary-50: 144, 202, 234;
|
||||
--color-primary-60: 109, 186, 227;
|
||||
--color-primary-70: 75, 170, 221;
|
||||
--color-primary-80: 41, 154, 214;
|
||||
--color-primary-90: 34, 129, 180;
|
||||
--color-primary-100: 0, 99, 153;
|
||||
--color-primary-200: 0, 92, 143;
|
||||
--color-primary-300: 0, 86, 133;
|
||||
--color-primary-400: 0, 77, 117;
|
||||
--color-primary-500: 0, 66, 102;
|
||||
--color-primary-600: 0, 53, 82;
|
||||
--color-primary-700: 0, 43, 66;
|
||||
--color-primary-800: 0, 33, 51;
|
||||
--color-primary-900: 0, 23, 36;
|
||||
|
||||
--color-background-100: 255, 255, 255; /* primary bg */
|
||||
--color-background-90: 247, 247, 247; /* secondary bg */
|
||||
--color-background-80: 232, 232, 232; /* tertiary bg */
|
||||
|
||||
--color-text-100: 23, 23, 23; /* primary text */
|
||||
--color-text-200: 58, 58, 58; /* secondary text */
|
||||
--color-text-300: 82, 82, 82; /* tertiary text */
|
||||
--color-text-400: 163, 163, 163; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 163, 163, 163; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 245, 245, 245; /* subtle border= 1 */
|
||||
--color-border-200: 229, 229, 229; /* subtle border- 2 */
|
||||
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
||||
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
||||
|
||||
--color-shadow-2xs:
|
||||
0px 0px 1px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.06),
|
||||
0px 1px 2px 0px rgba(23, 23, 23, 0.14);
|
||||
--color-shadow-xs:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.16), 0px 2px 4px 0px rgba(16, 24, 40, 0.12),
|
||||
0px 1px 8px -1px rgba(16, 24, 40, 0.1);
|
||||
--color-shadow-sm:
|
||||
0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02), 0px 1px 12px 0px rgba(0, 0, 0, 0.12);
|
||||
--color-shadow-rg:
|
||||
0px 3px 6px 0px rgba(0, 0, 0, 0.1), 0px 4px 4px 0px rgba(16, 24, 40, 0.08),
|
||||
0px 1px 12px 0px rgba(16, 24, 40, 0.04);
|
||||
--color-shadow-md:
|
||||
0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12),
|
||||
0px 1px 16px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-lg:
|
||||
0px 6px 12px 0px rgba(0, 0, 0, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 1px 24px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-xl:
|
||||
0px 0px 18px 0px rgba(0, 0, 0, 0.16), 0px 0px 24px 0px rgba(16, 24, 40, 0.16),
|
||||
0px 0px 52px 0px rgba(16, 24, 40, 0.16);
|
||||
--color-shadow-2xl:
|
||||
0px 8px 16px 0px rgba(0, 0, 0, 0.12), 0px 12px 24px 0px rgba(16, 24, 40, 0.12),
|
||||
0px 1px 32px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-3xl:
|
||||
0px 12px 24px 0px rgba(0, 0, 0, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 1px 48px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-4xl: 0px 8px 40px 0px rgba(0, 0, 61, 0.05), 0px 12px 32px -16px rgba(0, 0, 0, 0.05);
|
||||
|
||||
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
|
||||
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
|
||||
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
|
||||
|
||||
--color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
|
||||
--color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */
|
||||
--color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
|
||||
--color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */
|
||||
|
||||
--color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */
|
||||
--color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */
|
||||
--color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */
|
||||
--color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */
|
||||
|
||||
--color-sidebar-shadow-2xs: var(--color-shadow-2xs);
|
||||
--color-sidebar-shadow-xs: var(--color-shadow-xs);
|
||||
--color-sidebar-shadow-sm: var(--color-shadow-sm);
|
||||
--color-sidebar-shadow-rg: var(--color-shadow-rg);
|
||||
--color-sidebar-shadow-md: var(--color-shadow-md);
|
||||
--color-sidebar-shadow-lg: var(--color-shadow-lg);
|
||||
--color-sidebar-shadow-xl: var(--color-shadow-xl);
|
||||
--color-sidebar-shadow-2xl: var(--color-shadow-2xl);
|
||||
--color-sidebar-shadow-3xl: var(--color-shadow-3xl);
|
||||
--color-sidebar-shadow-4xl: var(--color-shadow-4xl);
|
||||
|
||||
/* toast theme */
|
||||
--color-toast-success-text: 178, 221, 181;
|
||||
--color-toast-error-text: 206, 44, 49;
|
||||
--color-toast-warning-text: 255, 186, 24;
|
||||
--color-toast-info-text: 141, 164, 239;
|
||||
--color-toast-loading-text: 255, 255, 255;
|
||||
--color-toast-secondary-text: 185, 187, 198;
|
||||
--color-toast-tertiary-text: 139, 141, 152;
|
||||
|
||||
--color-toast-success-background: 46, 46, 46;
|
||||
--color-toast-error-background: 46, 46, 46;
|
||||
--color-toast-warning-background: 46, 46, 46;
|
||||
--color-toast-info-background: 46, 46, 46;
|
||||
--color-toast-loading-background: 46, 46, 46;
|
||||
|
||||
--color-toast-success-border: 42, 126, 59;
|
||||
--color-toast-error-border: 100, 23, 35;
|
||||
--color-toast-warning-border: 79, 52, 34;
|
||||
--color-toast-info-border: 58, 91, 199;
|
||||
--color-toast-loading-border: 96, 100, 108;
|
||||
}
|
||||
|
||||
[data-theme="light"],
|
||||
[data-theme="light-contrast"] {
|
||||
color-scheme: light !important;
|
||||
|
||||
--color-background-100: 255, 255, 255; /* primary bg */
|
||||
--color-background-90: 247, 247, 247; /* secondary bg */
|
||||
--color-background-80: 232, 232, 232; /* tertiary bg */
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--color-text-100: 23, 23, 23; /* primary text */
|
||||
--color-text-200: 58, 58, 58; /* secondary text */
|
||||
--color-text-300: 82, 82, 82; /* tertiary text */
|
||||
--color-text-400: 163, 163, 163; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 163, 163, 163; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 245, 245, 245; /* subtle border= 1 */
|
||||
--color-border-200: 229, 229, 229; /* subtle border- 2 */
|
||||
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
||||
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
||||
|
||||
/* toast theme */
|
||||
--color-toast-success-text: 62, 155, 79;
|
||||
--color-toast-error-text: 220, 62, 66;
|
||||
--color-toast-warning-text: 255, 186, 24;
|
||||
--color-toast-info-text: 51, 88, 212;
|
||||
--color-toast-loading-text: 28, 32, 36;
|
||||
--color-toast-secondary-text: 128, 131, 141;
|
||||
--color-toast-tertiary-text: 96, 100, 108;
|
||||
|
||||
--color-toast-success-background: 253, 253, 254;
|
||||
--color-toast-error-background: 255, 252, 252;
|
||||
--color-toast-warning-background: 254, 253, 251;
|
||||
--color-toast-info-background: 253, 253, 254;
|
||||
--color-toast-loading-background: 253, 253, 254;
|
||||
|
||||
--color-toast-success-border: 218, 241, 219;
|
||||
--color-toast-error-border: 255, 219, 220;
|
||||
--color-toast-warning-border: 255, 247, 194;
|
||||
--color-toast-info-border: 210, 222, 255;
|
||||
--color-toast-loading-border: 224, 225, 230;
|
||||
}
|
||||
|
||||
[data-theme="light-contrast"] {
|
||||
--color-text-100: 11, 11, 11; /* primary text */
|
||||
--color-text-200: 38, 38, 38; /* secondary text */
|
||||
--color-text-300: 58, 58, 58; /* tertiary text */
|
||||
--color-text-400: 115, 115, 115; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 115, 115, 115; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 34, 34, 34; /* subtle border= 1 */
|
||||
--color-border-200: 38, 38, 38; /* subtle border- 2 */
|
||||
--color-border-300: 46, 46, 46; /* strong border- 1 */
|
||||
--color-border-400: 58, 58, 58; /* strong border- 2 */
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
[data-theme="dark-contrast"] {
|
||||
color-scheme: dark !important;
|
||||
|
||||
--color-primary-10: 8, 31, 43;
|
||||
--color-primary-20: 10, 37, 51;
|
||||
--color-primary-30: 13, 49, 69;
|
||||
--color-primary-40: 16, 58, 81;
|
||||
--color-primary-50: 18, 68, 94;
|
||||
--color-primary-60: 23, 86, 120;
|
||||
--color-primary-70: 28, 104, 146;
|
||||
--color-primary-80: 31, 116, 163;
|
||||
--color-primary-90: 34, 129, 180;
|
||||
--color-primary-100: 40, 146, 204;
|
||||
--color-primary-200: 41, 154, 214;
|
||||
--color-primary-300: 75, 170, 221;
|
||||
--color-primary-400: 109, 186, 227;
|
||||
--color-primary-500: 144, 202, 234;
|
||||
--color-primary-600: 169, 214, 239;
|
||||
--color-primary-700: 199, 229, 244;
|
||||
--color-primary-800: 216, 237, 248;
|
||||
--color-primary-900: 229, 243, 250;
|
||||
|
||||
--color-background-100: 25, 25, 25; /* primary bg */
|
||||
--color-background-90: 32, 32, 32; /* secondary bg */
|
||||
--color-background-80: 44, 44, 44; /* tertiary bg */
|
||||
|
||||
--color-shadow-2xs: 0px 0px 1px 0px rgba(0, 0, 0, 0.15), 0px 1px 3px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-xs: 0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-sm: 0px 0px 4px 0px rgba(0, 0, 0, 0.2), 0px 2px 6px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-rg: 0px 0px 6px 0px rgba(0, 0, 0, 0.2), 0px 4px 6px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-md: 0px 2px 8px 0px rgba(0, 0, 0, 0.2), 0px 4px 8px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-lg: 0px 4px 12px 0px rgba(0, 0, 0, 0.25), 0px 4px 10px 0px rgba(0, 0, 0, 0.55);
|
||||
--color-shadow-xl: 0px 0px 14px 0px rgba(0, 0, 0, 0.25), 0px 6px 10px 0px rgba(0, 0, 0, 0.55);
|
||||
--color-shadow-2xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.25), 0px 8px 12px 0px rgba(0, 0, 0, 0.6);
|
||||
--color-shadow-3xl: 0px 4px 24px 0px rgba(0, 0, 0, 0.3), 0px 12px 40px 0px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--color-text-100: 229, 229, 229; /* primary text */
|
||||
--color-text-200: 163, 163, 163; /* secondary text */
|
||||
--color-text-300: 115, 115, 115; /* tertiary text */
|
||||
--color-text-400: 82, 82, 82; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 82, 82, 82; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 34, 34, 34; /* subtle border= 1 */
|
||||
--color-border-200: 38, 38, 38; /* subtle border- 2 */
|
||||
--color-border-300: 46, 46, 46; /* strong border- 1 */
|
||||
--color-border-400: 58, 58, 58; /* strong border- 2 */
|
||||
}
|
||||
|
||||
[data-theme="dark-contrast"] {
|
||||
--color-text-100: 250, 250, 250; /* primary text */
|
||||
--color-text-200: 241, 241, 241; /* secondary text */
|
||||
--color-text-300: 212, 212, 212; /* tertiary text */
|
||||
--color-text-400: 115, 115, 115; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 115, 115, 115; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 245, 245, 245; /* subtle border= 1 */
|
||||
--color-border-200: 229, 229, 229; /* subtle border- 2 */
|
||||
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
||||
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
||||
}
|
||||
|
||||
[data-theme="light"],
|
||||
[data-theme="dark"],
|
||||
[data-theme="light-contrast"],
|
||||
[data-theme="dark-contrast"] {
|
||||
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
|
||||
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
|
||||
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
|
||||
|
||||
--color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
|
||||
--color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */
|
||||
--color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
|
||||
--color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */
|
||||
|
||||
--color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */
|
||||
--color-sidebar-border-200: var(--color-border-200); /* subtle sidebar border- 2 */
|
||||
--color-sidebar-border-300: var(--color-border-300); /* strong sidebar border- 1 */
|
||||
--color-sidebar-border-400: var(--color-border-400); /* strong sidebar border- 2 */
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
font-variant-ligatures: none;
|
||||
-webkit-font-variant-ligatures: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgba(var(--color-text-100));
|
||||
}
|
||||
|
||||
/* scrollbar style */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.vertical-scrollbar,
|
||||
.horizontal-scrollbar {
|
||||
scrollbar-width: initial;
|
||||
scrollbar-color: rgba(96, 100, 108, 0.1) transparent;
|
||||
}
|
||||
.vertical-scrollbar:hover,
|
||||
.horizontal-scrollbar:hover {
|
||||
scrollbar-color: rgba(96, 100, 108, 0.25) transparent;
|
||||
}
|
||||
.vertical-scrollbar:active,
|
||||
.horizontal-scrollbar:active {
|
||||
scrollbar-color: rgba(96, 100, 108, 0.7) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-scrollbar {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.horizontal-scrollbar {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar,
|
||||
.horizontal-scrollbar::-webkit-scrollbar {
|
||||
display: block;
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-track,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-thumb,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-clip: padding-box;
|
||||
background-color: rgba(96, 100, 108, 0.1);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.vertical-scrollbar:hover::-webkit-scrollbar-thumb,
|
||||
.horizontal-scrollbar:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(96, 100, 108, 0.25);
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-thumb:hover,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(96, 100, 108, 0.5);
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-thumb:active,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-thumb:active {
|
||||
background-color: rgba(96, 100, 108, 0.7);
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-corner,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
.vertical-scrollbar-margin-top-md::-webkit-scrollbar-track {
|
||||
margin-top: 44px;
|
||||
}
|
||||
|
||||
/* scrollbar xs size */
|
||||
.scrollbar-xs::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
.scrollbar-xs::-webkit-scrollbar-thumb {
|
||||
border: 3px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
@import "@plane/tailwind-config/index.css";
|
||||
|
||||
.shadow-custom {
|
||||
box-shadow: 2px 2px 8px 2px rgba(234, 231, 250, 0.3); /* Convert #EAE7FA4D to rgba */
|
||||
@@ -377,48 +8,15 @@ body {
|
||||
@apply backdrop-filter blur-[9px];
|
||||
}
|
||||
|
||||
/* scrollbar sm size */
|
||||
.scrollbar-sm::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
.scrollbar-sm::-webkit-scrollbar-thumb {
|
||||
border: 3px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
/* scrollbar md size */
|
||||
.scrollbar-md::-webkit-scrollbar {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
.scrollbar-md::-webkit-scrollbar-thumb {
|
||||
border: 3px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
/* scrollbar lg size */
|
||||
|
||||
.scrollbar-lg::-webkit-scrollbar {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
.scrollbar-lg::-webkit-scrollbar-thumb {
|
||||
border: 4px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
/* end scrollbar style */
|
||||
|
||||
/* progress bar */
|
||||
.progress-bar {
|
||||
fill: currentColor;
|
||||
color: rgba(var(--color-sidebar-background-100));
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder,
|
||||
::placeholder,
|
||||
:-ms-input-placeholder {
|
||||
color: rgb(var(--color-text-400));
|
||||
color: var(--background-color-surface-1);
|
||||
}
|
||||
|
||||
/* Progress Bar Styles */
|
||||
:root {
|
||||
--bprogress-color: rgb(var(--color-primary-100)) !important;
|
||||
--bprogress-color: var(--background-color-accent-primary);
|
||||
--bprogress-height: 2.5px !important;
|
||||
}
|
||||
|
||||
@@ -429,8 +27,8 @@ body {
|
||||
.bprogress .bar {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(var(--color-primary-100), 0.8) 0%,
|
||||
rgba(var(--color-primary-100), 1) 100%
|
||||
--alpha(var(--background-color-accent-primary) / 80%) 0%,
|
||||
--alpha(var(--background-color-accent-primary) / 100%) 100%
|
||||
) !important;
|
||||
will-change: width, opacity;
|
||||
}
|
||||
@@ -438,7 +36,7 @@ body {
|
||||
.bprogress .peg {
|
||||
display: block;
|
||||
box-shadow:
|
||||
0 0 8px rgba(var(--color-primary-100), 0.6),
|
||||
0 0 4px rgba(var(--color-primary-100), 0.4) !important;
|
||||
0 0 8px --alpha(var(--background-color-accent-primary) / 60%),
|
||||
0 0 4px --alpha(var(--background-color-accent-primary) / 40%) !important;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
const sharedConfig = require("@plane/tailwind-config/tailwind.config.js");
|
||||
|
||||
module.exports = {
|
||||
presets: [sharedConfig],
|
||||
};
|
||||
@@ -3,13 +3,7 @@ import random
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectIdentifier,
|
||||
WorkspaceMember,
|
||||
State,
|
||||
Estimate,
|
||||
)
|
||||
from plane.db.models import Project, ProjectIdentifier, WorkspaceMember, State, Estimate
|
||||
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
@@ -123,6 +117,7 @@ class ProjectCreateSerializer(BaseSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
identifier = validated_data.get("identifier", "").strip().upper()
|
||||
|
||||
if identifier == "":
|
||||
raise serializers.ValidationError(detail="Project Identifier is required")
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
|
||||
from plane.db.models import (
|
||||
Cycle,
|
||||
Intake,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
Module,
|
||||
Project,
|
||||
DeployBoard,
|
||||
@@ -210,14 +210,14 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
serializer = ProjectCreateSerializer(data={**request.data}, context={"workspace_id": workspace.id})
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
# Add the user as Administrator to the project
|
||||
_ = ProjectMember.objects.create(project_id=serializer.instance.id, member=request.user, role=20)
|
||||
# Also create the issue property for the user
|
||||
_ = IssueUserProperty.objects.create(project_id=serializer.instance.id, user=request.user)
|
||||
|
||||
if serializer.instance.project_lead is not None and str(serializer.instance.project_lead) != str(
|
||||
request.user.id
|
||||
@@ -227,11 +227,6 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
member_id=serializer.instance.project_lead,
|
||||
role=20,
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
IssueUserProperty.objects.create(
|
||||
project_id=serializer.instance.id,
|
||||
user_id=serializer.instance.project_lead,
|
||||
)
|
||||
|
||||
State.objects.bulk_create(
|
||||
[
|
||||
|
||||
@@ -52,7 +52,7 @@ from .issue import (
|
||||
IssueCreateSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
IssueUserPropertySerializer,
|
||||
ProjectUserPropertySerializer,
|
||||
IssueAssigneeSerializer,
|
||||
LabelSerializer,
|
||||
IssueSerializer,
|
||||
|
||||
@@ -18,7 +18,7 @@ from plane.db.models import (
|
||||
Issue,
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
IssueAssignee,
|
||||
IssueSubscriber,
|
||||
IssueLabel,
|
||||
@@ -346,9 +346,9 @@ class IssueActivitySerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class IssueUserPropertySerializer(BaseSerializer):
|
||||
class ProjectUserPropertySerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueUserProperty
|
||||
model = ProjectUserProperty
|
||||
fields = "__all__"
|
||||
read_only_fields = ["user", "workspace", "project"]
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer, DynamicBaseSerializer
|
||||
from django.db.models import Max
|
||||
from plane.app.serializers.workspace import WorkspaceLiteSerializer
|
||||
from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
|
||||
from plane.db.models import (
|
||||
@@ -12,6 +13,7 @@ from plane.db.models import (
|
||||
ProjectIdentifier,
|
||||
DeployBoard,
|
||||
ProjectPublicMember,
|
||||
IssueSequence,
|
||||
)
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
@@ -105,6 +107,7 @@ class ProjectListSerializer(DynamicBaseSerializer):
|
||||
members = serializers.SerializerMethodField()
|
||||
cover_image_url = serializers.CharField(read_only=True)
|
||||
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
|
||||
next_work_item_sequence = serializers.SerializerMethodField()
|
||||
|
||||
def get_members(self, obj):
|
||||
project_members = getattr(obj, "members_list", None)
|
||||
@@ -113,6 +116,11 @@ class ProjectListSerializer(DynamicBaseSerializer):
|
||||
return [member.member_id for member in project_members if member.is_active and not member.member.is_bot]
|
||||
return []
|
||||
|
||||
def get_next_work_item_sequence(self, obj):
|
||||
"""Get the next sequence ID that will be assigned to a new issue"""
|
||||
max_sequence = IssueSequence.objects.filter(project_id=obj.id).aggregate(max_seq=Max("sequence"))["max_seq"]
|
||||
return (max_sequence + 1) if max_sequence else 1
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = "__all__"
|
||||
|
||||
@@ -78,6 +78,7 @@ class UserMeSerializer(BaseSerializer):
|
||||
"is_password_autoset",
|
||||
"is_email_verified",
|
||||
"last_login_medium",
|
||||
"last_login_time",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from plane.app.views import (
|
||||
IssueReactionViewSet,
|
||||
IssueRelationViewSet,
|
||||
IssueSubscriberViewSet,
|
||||
IssueUserDisplayPropertyEndpoint,
|
||||
ProjectUserDisplayPropertyEndpoint,
|
||||
IssueViewSet,
|
||||
LabelViewSet,
|
||||
BulkArchiveIssuesEndpoint,
|
||||
@@ -208,13 +208,13 @@ urlpatterns = [
|
||||
name="project-issue-comment-reactions",
|
||||
),
|
||||
## End Comment Reactions
|
||||
## IssueUserProperty
|
||||
## ProjectUserProperty
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
|
||||
IssueUserDisplayPropertyEndpoint.as_view(),
|
||||
ProjectUserDisplayPropertyEndpoint.as_view(),
|
||||
name="project-issue-display-properties",
|
||||
),
|
||||
## IssueUserProperty End
|
||||
## ProjectUserProperty End
|
||||
## Issue Archives
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
|
||||
|
||||
@@ -114,7 +114,7 @@ from .asset.v2 import (
|
||||
from .issue.base import (
|
||||
IssueListEndpoint,
|
||||
IssueViewSet,
|
||||
IssueUserDisplayPropertyEndpoint,
|
||||
ProjectUserDisplayPropertyEndpoint,
|
||||
BulkDeleteIssuesEndpoint,
|
||||
DeletedIssuesListViewSet,
|
||||
IssuePaginatedViewSet,
|
||||
|
||||
@@ -34,7 +34,7 @@ from plane.app.serializers import (
|
||||
IssueDetailSerializer,
|
||||
IssueListDetailSerializer,
|
||||
IssueSerializer,
|
||||
IssueUserPropertySerializer,
|
||||
ProjectUserPropertySerializer,
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.bgtasks.issue_description_version_task import issue_description_version_task
|
||||
@@ -51,7 +51,7 @@ from plane.db.models import (
|
||||
IssueReaction,
|
||||
IssueRelation,
|
||||
IssueSubscriber,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
ModuleIssue,
|
||||
Project,
|
||||
ProjectMember,
|
||||
@@ -723,23 +723,33 @@ class IssueViewSet(BaseViewSet):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||
class ProjectUserDisplayPropertyEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def patch(self, request, slug, project_id):
|
||||
issue_property = IssueUserProperty.objects.get(user=request.user, project_id=project_id)
|
||||
try:
|
||||
issue_property = ProjectUserProperty.objects.get(
|
||||
user=request.user,
|
||||
project_id=project_id
|
||||
)
|
||||
except ProjectUserProperty.DoesNotExist:
|
||||
issue_property = ProjectUserProperty.objects.create(
|
||||
user=request.user,
|
||||
project_id=project_id
|
||||
)
|
||||
|
||||
issue_property.rich_filters = request.data.get("rich_filters", issue_property.rich_filters)
|
||||
issue_property.filters = request.data.get("filters", issue_property.filters)
|
||||
issue_property.display_filters = request.data.get("display_filters", issue_property.display_filters)
|
||||
issue_property.display_properties = request.data.get("display_properties", issue_property.display_properties)
|
||||
issue_property.save()
|
||||
serializer = IssueUserPropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
serializer = ProjectUserPropertySerializer(
|
||||
issue_property,
|
||||
data=request.data,
|
||||
partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id):
|
||||
issue_property, _ = IssueUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
|
||||
serializer = IssueUserPropertySerializer(issue_property)
|
||||
issue_property, _ = ProjectUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
|
||||
serializer = ProjectUserPropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user