chore: god mode settings.

This commit is contained in:
Prateek Shourya 2024-02-15 12:21:19 +05:30
parent 61b8ea4554
commit 5a6a754a70
36 changed files with 2320 additions and 216 deletions

View File

@ -1,28 +1,21 @@
"use client";
import { ReactElement } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// layouts
// import { InstanceAdminLayout } from "layouts/admin-layout";
// types
// import { NextPageWithLayout } from "lib/types";
// hooks
// import { useApplication } from "hooks/store";
import useInstance from "hooks/use-instance";
// ui
import { Loader } from "@plane/ui";
// icons
import { Lightbulb } from "lucide-react";
// components
// import { InstanceAIForm } from "components/instance";
import { InstanceAIForm } from "components/forms";
const InstanceAIPage = observer(() => {
// store
// const {
// instance: { fetchInstanceConfigurations, formattedConfig },
// } = useApplication();
const { fetchInstanceConfigurations, formattedConfig } = useInstance();
// useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return (
<div className="flex flex-col gap-8">
@ -35,7 +28,7 @@ const InstanceAIPage = observer(() => {
for all your workspaces.
</div>
</div>
{/* {formattedConfig ? (
{formattedConfig ? (
<>
<div>
<div className="pb-1 text-xl font-medium text-custom-text-100">
@ -64,7 +57,7 @@ const InstanceAIPage = observer(() => {
</div>
<Loader.Item height="50px" />
</Loader>
)} */}
)}
</div>
);
});

View File

@ -1,75 +1,67 @@
"use client";
import { ReactElement, useState } from "react";
import { useState } from "react";
import Link from "next/link";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// layouts
// import { InstanceAdminLayout } from "layouts/admin-layout";
// types
// import { NextPageWithLayout } from "lib/types";
// hooks
// import { useApplication } from "hooks/store";
import useInstance from "hooks/use-instance";
// hooks
// import useToast from "hooks/use-toast";
import useToast from "hooks/use-toast";
// ui
import { Loader, ToggleSwitch } from "@plane/ui";
// components
// import {
// InstanceGithubConfigForm,
// InstanceGoogleConfigForm,
// } from "components/instance";
import { InstanceGithubConfigForm, InstanceGoogleConfigForm } from "components/forms";
const InstanceAuthorizationPage = observer(() => {
// store
// const {
// instance: {
// fetchInstanceConfigurations,
// formattedConfig,
// updateInstanceConfigurations,
// },
// } = useApplication();
const {
fetchInstanceConfigurations,
formattedConfig,
updateInstanceConfigurations,
} = useInstance();
// useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
// toast
// const { setToastAlert } = useToast();
const { setToastAlert } = useToast();
// state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// const enableSignup = formattedConfig?.ENABLE_SIGNUP ?? "0";
// const enableMagicLogin = formattedConfig?.ENABLE_MAGIC_LINK_LOGIN ?? "0";
const enableSignup = formattedConfig?.ENABLE_SIGNUP ?? "0";
const enableMagicLogin = formattedConfig?.ENABLE_MAGIC_LINK_LOGIN ?? "0";
// const enableEmailPassword = formattedConfig?.ENABLE_EMAIL_PASSWORD ?? "0";
// const updateConfig = async (
// key: "ENABLE_SIGNUP" | "ENABLE_MAGIC_LINK_LOGIN" | "ENABLE_EMAIL_PASSWORD",
// value: string
// ) => {
// setIsSubmitting(true);
const updateConfig = async (
key: "ENABLE_SIGNUP" | "ENABLE_MAGIC_LINK_LOGIN" | "ENABLE_EMAIL_PASSWORD",
value: string
) => {
setIsSubmitting(true);
// const payload = {
// [key]: value,
// };
const payload = {
[key]: value,
};
// await updateInstanceConfigurations(payload)
// .then(() => {
// setToastAlert({
// title: "Success",
// type: "success",
// message: "SSO and OAuth Settings updated successfully",
// });
// setIsSubmitting(false);
// })
// .catch((err) => {
// console.error(err);
// setToastAlert({
// title: "Error",
// type: "error",
// message: "Failed to update SSO and OAuth Settings",
// });
// setIsSubmitting(false);
// });
// };
await updateInstanceConfigurations(payload)
.then(() => {
setToastAlert({
title: "Success",
type: "success",
message: "SSO and OAuth Settings updated successfully",
});
setIsSubmitting(false);
})
.catch((err) => {
console.error(err);
setToastAlert({
title: "Error",
type: "error",
message: "Failed to update SSO and OAuth Settings",
});
setIsSubmitting(false);
});
};
return (
<div className="flex flex-col gap-8">
@ -82,7 +74,7 @@ const InstanceAuthorizationPage = observer(() => {
and GitHub accounts, and below are the settings.
</div>
</div>
{/* {formattedConfig ? (
{formattedConfig ? (
<>
<div className="flex w-full flex-col gap-12 border-b border-custom-border-100 pb-8 lg:w-2/5">
<div className="pointer-events-none mr-4 flex items-center gap-14 opacity-50">
@ -166,7 +158,7 @@ const InstanceAuthorizationPage = observer(() => {
</div>
<Loader.Item height="50px" />
</Loader>
)} */}
)}
</div>
);
});

View File

@ -1,25 +1,19 @@
"use client";
import { ReactElement } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// layouts
// import { InstanceAdminLayout } from "layouts/admin-layout";
// hooks
// import { useApplication } from "hooks/store";
import useInstance from "hooks/use-instance";
// ui
import { Loader } from "@plane/ui";
// components
// import { InstanceEmailForm } from "components/instance";
import { InstanceEmailForm } from "components/forms";
const InstanceEmailPage = observer(() => {
// store
// const {
// instance: { fetchInstanceConfigurations, formattedConfig },
// } = useApplication();
const { fetchInstanceConfigurations, formattedConfig } = useInstance();
// useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return (
<div className="flex flex-col gap-8">
@ -38,7 +32,7 @@ const InstanceEmailPage = observer(() => {
</span>
</div>
</div>
{/* {formattedConfig ? (
{formattedConfig ? (
<InstanceEmailForm config={formattedConfig} />
) : (
<Loader className="space-y-4">
@ -48,7 +42,7 @@ const InstanceEmailPage = observer(() => {
</div>
<Loader.Item height="50px" />
</Loader>
)} */}
)}
</div>
);
});

View File

@ -4,3 +4,339 @@
@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: 236, 241, 255;
--color-primary-20: 217, 228, 255;
--color-primary-30: 197, 214, 255;
--color-primary-40: 178, 200, 255;
--color-primary-50: 159, 187, 255;
--color-primary-60: 140, 173, 255;
--color-primary-70: 121, 159, 255;
--color-primary-80: 101, 145, 255;
--color-primary-90: 82, 132, 255;
--color-primary-100: 63, 118, 255;
--color-primary-200: 57, 106, 230;
--color-primary-300: 50, 94, 204;
--color-primary-400: 44, 83, 179;
--color-primary-500: 38, 71, 153;
--color-primary-600: 32, 59, 128;
--color-primary-700: 25, 47, 102;
--color-primary-800: 19, 35, 76;
--color-primary-900: 13, 24, 51;
--color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 250, 250, 250; /* secondary bg */
--color-background-80: 245, 245, 245; /* 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);
}
[data-theme="light"],
[data-theme="light-contrast"] {
color-scheme: light !important;
--color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 250, 250, 250; /* secondary bg */
--color-background-80: 245, 245, 245; /* 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 */
/* onboarding colors */
--gradient-onboarding-100: linear-gradient(106deg, #f2f6ff 29.8%, #e1eaff 99.34%);
--gradient-onboarding-200: linear-gradient(129deg, rgba(255, 255, 255, 0) -22.23%, rgba(255, 255, 255, 0.8) 62.98%);
--gradient-onboarding-300: linear-gradient(164deg, #fff 4.25%, rgba(255, 255, 255, 0.06) 93.5%);
--gradient-onboarding-400: linear-gradient(129deg, rgba(255, 255, 255, 0) -22.23%, rgba(255, 255, 255, 0.8) 62.98%);
--color-onboarding-text-100: 23, 23, 23;
--color-onboarding-text-200: 58, 58, 58;
--color-onboarding-text-300: 82, 82, 82;
--color-onboarding-text-400: 163, 163, 163;
--color-onboarding-background-100: 236, 241, 255;
--color-onboarding-background-200: 255, 255, 255;
--color-onboarding-background-300: 236, 241, 255;
--color-onboarding-background-400: 177, 206, 250;
--color-onboarding-border-100: 229, 229, 229;
--color-onboarding-border-200: 217, 228, 255;
--color-onboarding-border-300: 229, 229, 229, 0.5;
--color-onboarding-shadow-sm: 0px 4px 20px 0px rgba(126, 139, 171, 0.1);
}
[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-background-100: 7, 7, 7; /* primary bg */
--color-background-90: 11, 11, 11; /* secondary bg */
--color-background-80: 23, 23, 23; /* 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 */
/* onboarding colors */
--gradient-onboarding-100: linear-gradient(106deg, #18191b 25.17%, #18191b 99.34%);
--gradient-onboarding-200: linear-gradient(129deg, rgba(47, 49, 53, 0.8) -22.23%, rgba(33, 34, 37, 0.8) 62.98%);
--gradient-onboarding-300: linear-gradient(167deg, rgba(47, 49, 53, 0.45) 19.22%, #212225 98.48%);
--color-onboarding-text-100: 237, 238, 240;
--color-onboarding-text-200: 176, 180, 187;
--color-onboarding-text-300: 118, 123, 132;
--color-onboarding-text-400: 105, 110, 119;
--color-onboarding-background-100: 54, 58, 64;
--color-onboarding-background-200: 40, 42, 45;
--color-onboarding-background-300: 40, 42, 45;
--color-onboarding-background-400: 67, 72, 79;
--color-onboarding-border-100: 54, 58, 64;
--color-onboarding-border-200: 54, 58, 64;
--color-onboarding-border-300: 34, 35, 38, 0.5;
--color-onboarding-shadow-sm: 0px 4px 20px 0px rgba(39, 44, 56, 0.1);
}
[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-primary-10: 236, 241, 255;
--color-primary-20: 217, 228, 255;
--color-primary-30: 197, 214, 255;
--color-primary-40: 178, 200, 255;
--color-primary-50: 159, 187, 255;
--color-primary-60: 140, 173, 255;
--color-primary-70: 121, 159, 255;
--color-primary-80: 101, 145, 255;
--color-primary-90: 82, 132, 255;
--color-primary-100: 63, 118, 255;
--color-primary-200: 57, 106, 230;
--color-primary-300: 50, 94, 204;
--color-primary-400: 44, 83, 179;
--color-primary-500: 38, 71, 153;
--color-primary-600: 32, 59, 128;
--color-primary-700: 25, 47, 102;
--color-primary-800: 19, 35, 76;
--color-primary-900: 13, 24, 51;
--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;
}
.horizontal-scroll-enable {
overflow-x: scroll;
}
.horizontal-scroll-enable::-webkit-scrollbar {
display: block;
height: 7px;
width: 0;
}
.horizontal-scroll-enable::-webkit-scrollbar-track {
height: 7px;
background-color: rgba(var(--color-background-100));
}
.horizontal-scroll-enable::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: rgba(var(--color-scrollbar));
}
.vertical-scroll-enable::-webkit-scrollbar {
display: block;
width: 5px;
}
.vertical-scroll-enable::-webkit-scrollbar-track {
width: 5px;
}
.vertical-scroll-enable::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: rgba(var(--color-background-90));
}
/* 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));
}

View File

@ -33,7 +33,7 @@ export const InstanceHeader: FC = observer(() => {
const title = getHeaderTitle();
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="relative z-10 flex h-[3.75rem] 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="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
{title && (
<div>

View File

@ -1,26 +1,19 @@
"use client";
import { ReactElement } from "react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// layouts
// import { InstanceAdminLayout } from "layouts/admin-layout";
// types
// import { NextPageWithLayout } from "lib/types";
// hooks
// import { useApplication } from "hooks/store";
import useInstance from "hooks/use-instance";
// ui
import { Loader } from "@plane/ui";
// components
// import { InstanceImageConfigForm } from "components/instance";
import { InstanceImageConfigForm } from "components/forms";
const InstanceImagePage = observer(() => {
// store
// const {
// instance: { fetchInstanceConfigurations, formattedConfig },
// } = useApplication();
const { fetchInstanceConfigurations, formattedConfig } = useInstance();
// useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
return (
<div className="flex flex-col gap-8">
@ -32,7 +25,7 @@ const InstanceImagePage = observer(() => {
Let your users search and choose images from third-party libraries
</div>
</div>
{/* {formattedConfig ? (
{formattedConfig ? (
<InstanceImageConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-4">
@ -42,7 +35,7 @@ const InstanceImagePage = observer(() => {
</div>
<Loader.Item height="50px" />
</Loader>
)} */}
)}
</div>
);
});

View File

@ -1,8 +1,11 @@
import "./globals.css";
// lib
import { ThemeProvider } from "lib/theme-provider";
import { ToastContextProvider } from "lib/toast-provider";
// components
import { InstanceSidebar } from "./sidebar";
import { InstanceHeader } from "./header";
// styles
import "./globals.css";
export const metadata = {
title: "God Mode",
@ -13,23 +16,19 @@ interface RootLayoutProps {
children: React.ReactNode;
}
const fetchAdminInfo = async () => {
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
const res = await fetch(BASE_URL + "/api/users/me/instance-admin/");
const data = await res.json();
return data;
};
export default async function RootLayout({ children }: RootLayoutProps) {
const response = await fetchAdminInfo();
console.log(response);
export const RootLayout = async ({ children }: RootLayoutProps) => {
const isUserInstanceAdmin = true;
return (
<html lang="en">
<body className={`antialiased`}>
{/* <AuthWrapper> */}
{/* {response?.is_instance_admin ? (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{/* {isUserInstanceAdmin || true ? ( */}
<ThemeProvider
themes={["light", "dark"]}
defaultTheme="system"
enableSystem
>
<ToastContextProvider>
<div className="relative flex h-screen w-full overflow-hidden">
<InstanceSidebar />
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
@ -41,24 +40,14 @@ export default async function RootLayout({ children }: RootLayoutProps) {
</div>
</main>
</div>
</ThemeProvider>
) : (
</ToastContextProvider>
</ThemeProvider>
{/* ) : (
<div>Login</div>
)} */}
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<div className="relative flex h-screen w-full overflow-hidden">
<InstanceSidebar />
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
<InstanceHeader />
<div className="h-full w-full overflow-hidden px-10 py-12">
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
{children}
</div>
</div>
</main>
</div>
</ThemeProvider>
</body>
</html>
);
}
};
export default RootLayout;

View File

@ -1,6 +1,38 @@
"use client";
import useSWR from "swr";
import useSWRImmutable from "swr/immutable";
// hooks
import useUser from "hooks/use-user";
import useInstance from "hooks/use-instance";
// components
import { GeneralView } from "components/views";
export default function Home() {
const {
// isUserInstanceAdmin,
fetchCurrentUser,
fetchCurrentUserInstanceAdminStatus,
} = useUser();
const { fetchInstanceInfo, fetchInstanceAdmins } = useInstance();
// fetching user information
useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
shouldRetryOnError: false,
});
// fetching current user instance admin status
useSWRImmutable(
"CURRENT_USER_INSTANCE_ADMIN_STATUS",
() => fetchCurrentUserInstanceAdminStatus(),
{
shouldRetryOnError: false,
}
);
// fetching instance information
useSWR("INSTANCE_INFO", () => fetchInstanceInfo());
// fetching instance admins
useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins());
return (
<div className="flex">
<GeneralView />

View File

@ -1,14 +1,15 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useAppTheme } from "hooks/useTheme";
import useAppTheme from "hooks/use-theme";
// components
import { HelpSection, SidebarMenu, SidebarDropdown } from "components/sidebar";
export interface IInstanceSidebar {}
export const InstanceSidebar: FC<IInstanceSidebar> = () => {
export const InstanceSidebar: FC<IInstanceSidebar> = observer(() => {
// store
const { sidebarCollapsed } = useAppTheme();
@ -25,4 +26,4 @@ export const InstanceSidebar: FC<IInstanceSidebar> = () => {
</div>
</div>
);
};
});

View File

@ -0,0 +1,151 @@
import { FC, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Eye, EyeOff } from "lucide-react";
// ui
import { Button, Input } from "@plane/ui";
// types
import { IFormattedInstanceConfiguration } from "@plane/types";
// hooks
import useInstance from "hooks/use-instance";
import useToast from "hooks/use-toast";
export interface IInstanceAIForm {
config: IFormattedInstanceConfiguration;
}
export interface AIFormValues {
OPENAI_API_KEY: string;
GPT_ENGINE: string;
}
export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
const { config } = props;
// states
const [showPassword, setShowPassword] = useState(false);
// store
const { updateInstanceConfigurations } = useInstance();
// toast
const { setToastAlert } = useToast();
// form data
const {
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<AIFormValues>({
defaultValues: {
OPENAI_API_KEY: config["OPENAI_API_KEY"],
GPT_ENGINE: config["GPT_ENGINE"],
},
});
const onSubmit = async (formData: AIFormValues) => {
const payload: Partial<AIFormValues> = { ...formData };
await updateInstanceConfigurations(payload)
.then(() =>
setToastAlert({
title: "Success",
type: "success",
message: "AI Settings updated successfully",
})
)
.catch((err) => console.error(err));
};
return (
<>
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-x-16 gap-y-8 lg:grid-cols-3">
<div className="flex flex-col gap-1">
<h4 className="text-sm">GPT_ENGINE</h4>
<Controller
control={control}
name="GPT_ENGINE"
render={({ field: { value, onChange, ref } }) => (
<Input
id="GPT_ENGINE"
name="GPT_ENGINE"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.GPT_ENGINE)}
placeholder="gpt-3.5-turbo"
className="w-full rounded-md font-medium"
/>
)}
/>
<p className="text-xs text-custom-text-400">
Choose an OpenAI engine.{" "}
<a
href="https://platform.openai.com/docs/models/overview"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Learn more
</a>
</p>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">API key</h4>
<div className="relative">
<Controller
control={control}
name="OPENAI_API_KEY"
render={({ field: { value, onChange, ref } }) => (
<Input
id="OPENAI_API_KEY"
name="OPENAI_API_KEY"
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.OPENAI_API_KEY)}
placeholder="sk-asddassdfasdefqsdfasd23das3dasdcasd"
className="w-full rounded-md !pr-10 font-medium"
/>
)}
/>
{showPassword ? (
<button
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(false)}
>
<EyeOff className="h-4 w-4" />
</button>
) : (
<button
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(true)}
>
<Eye className="h-4 w-4" />
</button>
)}
</div>
<p className="text-xs text-custom-text-400">
You will find your API key{" "}
<a
href="https://platform.openai.com/api-keys"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
</a>
</p>
</div>
</div>
<div className="flex items-center py-1">
<Button
variant="primary"
onClick={handleSubmit(onSubmit)}
loading={isSubmitting}
>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
</div>
</>
);
};

View File

@ -0,0 +1,266 @@
import { FC, useState } from "react";
import { Controller, useForm } from "react-hook-form";
// ui
import { Button, Input, ToggleSwitch } from "@plane/ui";
import { Eye, EyeOff } from "lucide-react";
// types
import { IFormattedInstanceConfiguration } from "@plane/types";
// hooks
import useInstance from "hooks/use-instance";
import useToast from "hooks/use-toast";
export interface IInstanceEmailForm {
config: IFormattedInstanceConfiguration;
}
export interface EmailFormValues {
EMAIL_HOST: string;
EMAIL_PORT: string;
EMAIL_HOST_USER: string;
EMAIL_HOST_PASSWORD: string;
EMAIL_USE_TLS: string;
// EMAIL_USE_SSL: string;
EMAIL_FROM: string;
}
export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
const { config } = props;
// states
const [showPassword, setShowPassword] = useState(false);
// store hooks
const { updateInstanceConfigurations } = useInstance();
// toast
const { setToastAlert } = useToast();
// form data
const {
handleSubmit,
watch,
control,
formState: { errors, isSubmitting },
} = useForm<EmailFormValues>({
defaultValues: {
EMAIL_HOST: config["EMAIL_HOST"],
EMAIL_PORT: config["EMAIL_PORT"],
EMAIL_HOST_USER: config["EMAIL_HOST_USER"],
EMAIL_HOST_PASSWORD: config["EMAIL_HOST_PASSWORD"],
EMAIL_USE_TLS: config["EMAIL_USE_TLS"],
// EMAIL_USE_SSL: config["EMAIL_USE_SSL"],
EMAIL_FROM: config["EMAIL_FROM"],
},
});
const onSubmit = async (formData: EmailFormValues) => {
const payload: Partial<EmailFormValues> = { ...formData };
await updateInstanceConfigurations(payload)
.then(() =>
setToastAlert({
title: "Success",
type: "success",
message: "Email Settings updated successfully",
})
)
.catch((err) => console.error(err));
};
return (
<>
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-center justify-between gap-x-20 gap-y-10 lg:grid-cols-2">
<div className="flex flex-col gap-1">
<h4 className="text-sm">Host</h4>
<Controller
control={control}
name="EMAIL_HOST"
render={({ field: { value, onChange, ref } }) => (
<Input
id="EMAIL_HOST"
name="EMAIL_HOST"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.EMAIL_HOST)}
placeholder="email.google.com"
className="w-full rounded-md font-medium"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Port</h4>
<Controller
control={control}
name="EMAIL_PORT"
render={({ field: { value, onChange, ref } }) => (
<Input
id="EMAIL_PORT"
name="EMAIL_PORT"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.EMAIL_PORT)}
placeholder="8080"
className="w-full rounded-md font-medium"
/>
)}
/>
</div>
</div>
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-center justify-between gap-x-20 gap-y-10 lg:grid-cols-2">
<div className="flex flex-col gap-1">
<h4 className="text-sm">Username</h4>
<Controller
control={control}
name="EMAIL_HOST_USER"
render={({ field: { value, onChange, ref } }) => (
<Input
id="EMAIL_HOST_USER"
name="EMAIL_HOST_USER"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.EMAIL_HOST_USER)}
placeholder="getitdone@projectplane.so"
className="w-full rounded-md font-medium"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Password</h4>
<div className="relative">
<Controller
control={control}
name="EMAIL_HOST_PASSWORD"
render={({ field: { value, onChange, ref } }) => (
<Input
id="EMAIL_HOST_PASSWORD"
name="EMAIL_HOST_PASSWORD"
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.EMAIL_HOST_PASSWORD)}
placeholder="Password"
className="w-full rounded-md !pr-10 font-medium"
/>
)}
/>
{showPassword ? (
<button
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(false)}
>
<EyeOff className="h-4 w-4" />
</button>
) : (
<button
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(true)}
>
<Eye className="h-4 w-4" />
</button>
)}
</div>
</div>
</div>
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-center justify-between gap-x-20 gap-y-10 lg:grid-cols-2">
<div className="flex flex-col gap-1">
<h4 className="text-sm">From address</h4>
<Controller
control={control}
name="EMAIL_FROM"
render={({ field: { value, onChange, ref } }) => (
<Input
id="EMAIL_FROM"
name="EMAIL_FROM"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.EMAIL_FROM)}
placeholder="no-reply@projectplane.so"
className="w-full rounded-md font-medium"
/>
)}
/>
<p className="text-xs text-custom-text-400">
This is the email address your users will see when getting emails
from this instance. You will need to verify this address.
</p>
</div>
</div>
<div className="flex w-full max-w-md 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">
Turn TLS{" "}
{Boolean(parseInt(watch("EMAIL_USE_TLS"))) ? "off" : "on"}
</div>
<div className="text-xs font-normal text-custom-text-300">
Use this if your email domain supports TLS.
</div>
</div>
<div className="shrink-0">
<Controller
control={control}
name="EMAIL_USE_TLS"
render={({ field: { value, onChange } }) => (
<ToggleSwitch
value={Boolean(parseInt(value))}
onChange={() => {
Boolean(parseInt(value)) === true
? onChange("0")
: onChange("1");
}}
size="sm"
/>
)}
/>
</div>
</div>
{/* <div className="flex items-center gap-10 pt-4 mr-8">
<div className="grow">
<div className="text-custom-text-100 font-medium text-sm">
Turn SSL {Boolean(parseInt(watch("EMAIL_USE_SSL"))) ? "off" : "on"}
</div>
<div className="text-custom-text-300 font-normal text-xs">
Most email domains support SSL. Use this to secure comms between this instance and your users.
</div>
</div>
<div className="shrink-0">
<Controller
control={control}
name="EMAIL_USE_SSL"
render={({ field: { value, onChange } }) => (
<ToggleSwitch
value={Boolean(parseInt(value))}
onChange={() => {
Boolean(parseInt(value)) === true ? onChange("0") : onChange("1");
}}
size="sm"
/>
)}
/>
</div>
</div> */}
</div>
<div className="flex max-w-4xl items-center py-1">
<Button
variant="primary"
onClick={handleSubmit(onSubmit)}
loading={isSubmitting}
>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
</div>
</>
);
};

View File

@ -0,0 +1,130 @@
import { FC } from "react";
import { Controller, useForm } from "react-hook-form";
// ui
import { Button, Input } from "@plane/ui";
// types
import { IInstance, IInstanceAdmin } from "@plane/types";
// hooks
import useInstance from "hooks/use-instance";
import useToast from "hooks/use-toast";
export interface IInstanceGeneralForm {
instance: IInstance;
instanceAdmins: IInstanceAdmin[];
}
export interface GeneralFormValues {
instance_name: string;
// is_telemetry_enabled: boolean;
}
export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
const { instance, instanceAdmins } = props;
// store hooks
const { updateInstanceInfo } = useInstance();
// toast
const { setToastAlert } = useToast();
// form data
const {
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<GeneralFormValues>({
defaultValues: {
instance_name: instance.instance_name,
// is_telemetry_enabled: instance.is_telemetry_enabled,
},
});
const onSubmit = async (formData: GeneralFormValues) => {
const payload: Partial<GeneralFormValues> = { ...formData };
await updateInstanceInfo(payload)
.then(() =>
setToastAlert({
title: "Success",
type: "success",
message: "Settings updated successfully",
})
)
.catch((err) => console.error(err));
};
return (
<>
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-8 md:grid-cols-2 lg:grid-cols-3">
<div className="flex flex-col gap-1">
<h4 className="text-sm">Name of instance</h4>
<Controller
control={control}
name="instance_name"
render={({ field: { value, onChange, ref } }) => (
<Input
id="instance_name"
name="instance_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.instance_name)}
placeholder="Instance Name"
className="w-full rounded-md font-medium"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Admin 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"
disabled
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">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"
disabled
/>
</div>
</div>
{/* <div className="flex items-center gap-12 pt-4">
<div>
<div className="text-custom-text-100 font-medium text-sm">Share anonymous usage instance</div>
<div className="text-custom-text-300 font-normal text-xs">
Help us understand how you use Plane so we can build better for you.
</div>
</div>
<div>
<Controller
control={control}
name="is_telemetry_enabled"
render={({ field: { value, onChange } }) => <ToggleSwitch value={value} onChange={onChange} size="sm" />}
/>
</div>
</div> */}
<div className="flex items-center py-1">
<Button
variant="primary"
onClick={handleSubmit(onSubmit)}
loading={isSubmitting}
>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
</div>
</>
);
};

View File

@ -0,0 +1,187 @@
import { FC, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Copy, Eye, EyeOff } from "lucide-react";
// ui
import { Button, Input } from "@plane/ui";
// types
import { IFormattedInstanceConfiguration } from "@plane/types";
// hooks
import useInstance from "hooks/use-instance";
import useToast from "hooks/use-toast";
export interface IInstanceGithubConfigForm {
config: IFormattedInstanceConfiguration;
}
export interface GithubConfigFormValues {
GITHUB_CLIENT_ID: string;
GITHUB_CLIENT_SECRET: string;
}
export const InstanceGithubConfigForm: FC<IInstanceGithubConfigForm> = (
props
) => {
const { config } = props;
// states
const [showPassword, setShowPassword] = useState(false);
// store hooks
const { updateInstanceConfigurations } = useInstance();
// toast
const { setToastAlert } = useToast();
// form data
const {
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<GithubConfigFormValues>({
defaultValues: {
GITHUB_CLIENT_ID: config["GITHUB_CLIENT_ID"],
GITHUB_CLIENT_SECRET: config["GITHUB_CLIENT_SECRET"],
},
});
const onSubmit = async (formData: GithubConfigFormValues) => {
const payload: Partial<GithubConfigFormValues> = { ...formData };
await updateInstanceConfigurations(payload)
.then(() =>
setToastAlert({
title: "Success",
type: "success",
message: "Github Configuration Settings updated successfully",
})
)
.catch((err) => console.error(err));
};
const originURL = typeof window !== "undefined" ? window.location.origin : "";
return (
<div className="flex flex-col gap-8">
<div className="grid-col grid w-full grid-cols-1 justify-between gap-x-12 gap-y-8 lg:grid-cols-3">
<div className="flex flex-col gap-1">
<h4 className="text-sm">Client ID</h4>
<Controller
control={control}
name="GITHUB_CLIENT_ID"
render={({ field: { value, onChange, ref } }) => (
<Input
id="GITHUB_CLIENT_ID"
name="GITHUB_CLIENT_ID"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.GITHUB_CLIENT_ID)}
placeholder="70a44354520df8bd9bcd"
className="w-full rounded-md font-medium"
/>
)}
/>
<p className="text-xs text-custom-text-400">
You will get this from your{" "}
<a
href="https://github.com/settings/applications/new"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitHub OAuth application settings.
</a>
</p>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Client secret</h4>
<div className="relative">
<Controller
control={control}
name="GITHUB_CLIENT_SECRET"
render={({ field: { value, onChange, ref } }) => (
<Input
id="GITHUB_CLIENT_SECRET"
name="GITHUB_CLIENT_SECRET"
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.GITHUB_CLIENT_SECRET)}
placeholder="9b0050f94ec1b744e32ce79ea4ffacd40d4119cb"
className="w-full rounded-md !pr-10 font-medium"
/>
)}
/>
{showPassword ? (
<button
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(false)}
>
<EyeOff className="h-4 w-4" />
</button>
) : (
<button
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(true)}
>
<Eye className="h-4 w-4" />
</button>
)}
</div>
<p className="text-xs text-custom-text-400">
Your client secret is also found in your{" "}
<a
href="https://github.com/settings/applications/new"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitHub OAuth application settings.
</a>
</p>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Origin URL</h4>
<Button
variant="neutral-primary"
className="flex items-center justify-between py-2"
onClick={() => {
navigator.clipboard.writeText(originURL);
setToastAlert({
message:
"The Origin URL has been successfully copied to your clipboard",
type: "success",
title: "Copied to clipboard",
});
}}
>
<p className="text-sm font-medium">{originURL}</p>
<Copy size={18} color="#B9B9B9" />
</Button>
<p className="text-xs text-custom-text-400">
We will auto-generate this. Paste this into the Authorization
callback URL field{" "}
<a
href="https://github.com/settings/applications/new"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
</a>
</p>
</div>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center">
<Button
variant="primary"
onClick={handleSubmit(onSubmit)}
loading={isSubmitting}
>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,136 @@
import { FC } from "react";
import { Controller, useForm } from "react-hook-form";
import { Copy } from "lucide-react";
// ui
import { Button, Input } from "@plane/ui";
// types
import { IFormattedInstanceConfiguration } from "@plane/types";
// hooks
import useInstance from "hooks/use-instance";
import useToast from "hooks/use-toast";
export interface IInstanceGoogleConfigForm {
config: IFormattedInstanceConfiguration;
}
export interface GoogleConfigFormValues {
GOOGLE_CLIENT_ID: string;
GOOGLE_CLIENT_SECRET: string;
}
export const InstanceGoogleConfigForm: FC<IInstanceGoogleConfigForm> = (
props
) => {
const { config } = props;
// store hooks
const { updateInstanceConfigurations } = useInstance();
// toast
const { setToastAlert } = useToast();
// form data
const {
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<GoogleConfigFormValues>({
defaultValues: {
GOOGLE_CLIENT_ID: config["GOOGLE_CLIENT_ID"],
GOOGLE_CLIENT_SECRET: config["GOOGLE_CLIENT_SECRET"],
},
});
const onSubmit = async (formData: GoogleConfigFormValues) => {
const payload: Partial<GoogleConfigFormValues> = { ...formData };
await updateInstanceConfigurations(payload)
.then(() =>
setToastAlert({
title: "Success",
type: "success",
message: "Google Configuration Settings updated successfully",
})
)
.catch((err) => console.error(err));
};
const originURL = typeof window !== "undefined" ? window.location.origin : "";
return (
<div className="flex flex-col gap-8">
<div className="grid-col grid w-full grid-cols-1 justify-between gap-x-12 gap-y-8 lg:grid-cols-3">
<div className="flex flex-col gap-1">
<h4 className="text-sm">Client ID</h4>
<Controller
control={control}
name="GOOGLE_CLIENT_ID"
render={({ field: { value, onChange, ref } }) => (
<Input
id="GOOGLE_CLIENT_ID"
name="GOOGLE_CLIENT_ID"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.GOOGLE_CLIENT_ID)}
placeholder="840195096245-0p2tstej9j5nc4l8o1ah2dqondscqc1g.apps.googleusercontent.com"
className="w-full rounded-md font-medium"
/>
)}
/>
<p className="text-xs text-custom-text-400">
Your client ID lives in your Google API Console.{" "}
<a
href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Learn more
</a>
</p>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">JavaScript origin URL</h4>
<Button
variant="neutral-primary"
className="flex items-center justify-between py-2"
onClick={() => {
navigator.clipboard.writeText(originURL);
setToastAlert({
message:
"The Origin URL has been successfully copied to your clipboard",
type: "success",
title: "Copied to clipboard",
});
}}
>
<p className="text-sm font-medium">{originURL}</p>
<Copy size={18} color="#B9B9B9" />
</Button>
<p className="text-xs text-custom-text-400">
We will auto-generate this. Paste this into your Authorized
JavaScript origins field. For this OAuth client{" "}
<a
href="https://console.cloud.google.com/apis/credentials/oauthclient"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
here.
</a>
</p>
</div>
</div>
<div className="flex flex-col gap-1">
<div className="flex items-center">
<Button
variant="primary"
onClick={handleSubmit(onSubmit)}
loading={isSubmitting}
>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,119 @@
import { FC, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Eye, EyeOff } from "lucide-react";
// ui
import { Button, Input } from "@plane/ui";
// types
import { IFormattedInstanceConfiguration } from "@plane/types";
// hooks
import useInstance from "hooks/use-instance";
import useToast from "hooks/use-toast";
export interface IInstanceImageConfigForm {
config: IFormattedInstanceConfiguration;
}
export interface ImageConfigFormValues {
UNSPLASH_ACCESS_KEY: string;
}
export const InstanceImageConfigForm: FC<IInstanceImageConfigForm> = (
props
) => {
const { config } = props;
// states
const [showPassword, setShowPassword] = useState(false);
// store hooks
const { updateInstanceConfigurations } = useInstance();
// toast
const { setToastAlert } = useToast();
// form data
const {
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<ImageConfigFormValues>({
defaultValues: {
UNSPLASH_ACCESS_KEY: config["UNSPLASH_ACCESS_KEY"],
},
});
const onSubmit = async (formData: ImageConfigFormValues) => {
const payload: Partial<ImageConfigFormValues> = { ...formData };
await updateInstanceConfigurations(payload)
.then(() =>
setToastAlert({
title: "Success",
type: "success",
message: "Image Configuration Settings updated successfully",
})
)
.catch((err) => console.error(err));
};
return (
<>
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-x-16 gap-y-8 lg:grid-cols-2">
<div className="flex max-w-md flex-col gap-1">
<h4 className="text-sm">Access key from your Unsplash account</h4>
<div className="relative">
<Controller
control={control}
name="UNSPLASH_ACCESS_KEY"
render={({ field: { value, onChange, ref } }) => (
<Input
id="UNSPLASH_ACCESS_KEY"
name="UNSPLASH_ACCESS_KEY"
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.UNSPLASH_ACCESS_KEY)}
placeholder="oXgq-sdfadsaeweqasdfasdf3234234rassd"
className="w-full rounded-md !pr-10 font-medium"
/>
)}
/>
{showPassword ? (
<button
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(false)}
>
<EyeOff className="h-4 w-4" />
</button>
) : (
<button
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(true)}
>
<Eye className="h-4 w-4" />
</button>
)}
</div>
<p className="text-xs text-custom-text-400">
You will find your access key in your Unsplash developer console.{" "}
<a
href="https://unsplash.com/documentation#creating-a-developer-account"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
Learn more.
</a>
</p>
</div>
</div>
<div className="flex items-center py-1">
<Button
variant="primary"
onClick={handleSubmit(onSubmit)}
loading={isSubmitting}
>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
</div>
</>
);
};

View File

@ -0,0 +1,6 @@
export * from "./general-form";
export * from "./ai-form";
export * from "./email-form";
export * from "./github-config-form";
export * from "./google-config-form";
export * from "./image-config-form";

View File

@ -3,7 +3,7 @@ import { Transition } from "@headlessui/react";
import Link from "next/link";
import { FileText, HelpCircle, MessagesSquare, MoveLeft } from "lucide-react";
// hooks
import { useAppTheme } from "hooks/useTheme";
import { useAppTheme } from "hooks/use-theme";
// icons
import { DiscordIcon, GithubIcon } from "@plane/ui";
// assets
@ -43,7 +43,7 @@ export const HelpSection: FC = () => {
return (
<div
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
sidebarCollapsed ? "flex-col" : ""
}`}
>

View File

@ -1,60 +1,48 @@
import { Fragment } from "react";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { useTheme } from "next-themes";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { mutate } from "swr";
// components
import { Menu, Transition } from "@headlessui/react";
// icons
import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react";
import { LogOut, UserCog2, Palette } from "lucide-react";
// hooks
import { useAppTheme } from "hooks/useTheme";
// hooks
// import useToast from "hooks/use-toast";
import { useAppTheme } from "hooks/use-theme";
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
// ui
import { Avatar, Tooltip } from "@plane/ui";
// Static Data
const PROFILE_LINKS = [
{
key: "settings",
name: "Settings",
icon: Settings,
link: `/profile`,
},
];
import { Avatar } from "@plane/ui";
export const SidebarDropdown = observer(() => {
// router
// const router = useRouter();
const router = useRouter();
// store hooks
const { sidebarCollapsed } = useAppTheme();
// const { signOut, currentUser, currentUserSettings } = useUser();
const { signOut, currentUser } = useUser();
// hooks
// const { setToastAlert } = useToast();
const { setTheme } = useTheme();
// redirect url for normal mode
// const redirectWorkspaceSlug =
// workspaceSlug ||
// currentUserSettings?.workspace?.last_workspace_slug ||
// currentUserSettings?.workspace?.fallback_workspace_slug ||
// "";
const { setToastAlert } = useToast();
const { resolvedTheme, setTheme } = useTheme();
const handleSignOut = async () => {
// await signOut()
// .then(() => {
// mutate("CURRENT_USER_DETAILS", null);
// setTheme("system");
// router.push("/");
// })
// .catch(() =>
// setToastAlert({
// type: "error",
// title: "Error!",
// message: "Failed to sign out. Please try again.",
// })
// );
await signOut()
.then(() => {
mutate("CURRENT_USER_DETAILS", null);
setTheme("system");
router.push("/");
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Failed to sign out. Please try again.",
})
);
};
const handleThemeSwitch = () => {
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
setTheme(newTheme);
};
return (
@ -74,30 +62,21 @@ export const SidebarDropdown = observer(() => {
<h4 className="grow truncate text-base font-medium text-custom-text-200">
Instance admin
</h4>
<Tooltip position="bottom-left" tooltipContent="Exit God Mode">
<div className="flex-shrink-0">
{/* <Link href={`/${redirectWorkspaceSlug}`}>
<span>
<LogIn className="h-5 w-5 rotate-180 text-custom-text-200" />
</span>
</Link> */}
</div>
</Tooltip>
</div>
)}
</div>
</div>
{!sidebarCollapsed && (
{!sidebarCollapsed && currentUser && (
<Menu as="div" className="relative flex-shrink-0">
<Menu.Button className="grid place-items-center outline-none">
{/* <Avatar
name={currentUser?.display_name}
src={currentUser?.avatar}
<Avatar
name={currentUser.display_name}
src={currentUser.avatar}
size={24}
shape="square"
className="!text-base"
/> */}
/>
</Menu.Button>
<Transition
@ -114,19 +93,20 @@ export const SidebarDropdown = observer(() => {
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"
>
<div className="flex flex-col gap-2.5 pb-2">
{/* <span className="px-2 text-custom-sidebar-text-200">
<span className="px-2 text-custom-sidebar-text-200">
{currentUser?.email}
</span> */}
{PROFILE_LINKS.map((link) => (
<Menu.Item key={link.key} as="button" type="button">
<Link href={link.link}>
<span className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
<link.icon className="h-4 w-4 stroke-[1.5]" />
{link.name}
</span>
</Link>
</Menu.Item>
))}
</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"
onClick={handleThemeSwitch}
>
<Palette className="h-4 w-4 stroke-[1.5]" />
Switch to {resolvedTheme === "dark" ? "light" : "dark"} mode
</Menu.Item>
</div>
<div className="py-2">
<Menu.Item
@ -139,16 +119,6 @@ export const SidebarDropdown = observer(() => {
Sign out
</Menu.Item>
</div>
<div className="p-2 pb-0">
<Menu.Item as="button" type="button" className="w-full">
{/* <Link href={`/${redirectWorkspaceSlug}`}>
<span className="flex w-full items-center justify-center rounded bg-custom-primary-100/20 px-2 py-1 text-sm font-medium text-custom-primary-100 hover:bg-custom-primary-100/30 hover:text-custom-primary-200">
Exit God Mode
</span>
</Link> */}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>

View File

@ -2,7 +2,7 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
// hooks
import { useAppTheme } from "hooks/useTheme";
import { useAppTheme } from "hooks/use-theme";
// ui
import { Tooltip } from "@plane/ui";
import { observer } from "mobx-react-lite";

View File

@ -0,0 +1,61 @@
import React from "react";
// hooks
import useToast from "hooks/use-toast";
// icons
import { AlertTriangle, CheckCircle, Info, X, XCircle } from "lucide-react";
const ToastAlerts = () => {
const { alerts, removeAlert } = useToast();
if (!alerts) return null;
return (
<div className="pointer-events-none fixed right-5 top-5 z-50 h-full w-80 space-y-5 overflow-hidden">
{alerts.map((alert) => (
<div className="relative overflow-hidden rounded-md text-white" key={alert.id}>
<div className="absolute right-1 top-1">
<button
type="button"
className="pointer-events-auto inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2"
onClick={() => removeAlert(alert.id)}
>
<span className="sr-only">Dismiss</span>
<X className="h-5 w-5" aria-hidden="true" />
</button>
</div>
<div
className={`px-2 py-4 ${
alert.type === "success"
? "bg-[#06d6a0]"
: alert.type === "error"
? "bg-[#ef476f]"
: alert.type === "warning"
? "bg-[#e98601]"
: "bg-[#1B9aaa]"
}`}
>
<div className="flex items-center gap-x-3">
<div className="flex-shrink-0">
{alert.type === "success" ? (
<CheckCircle className="h-8 w-8" aria-hidden="true" />
) : alert.type === "error" ? (
<XCircle className="h-8 w-8" />
) : alert.type === "warning" ? (
<AlertTriangle className="h-8 w-8" aria-hidden="true" />
) : (
<Info className="h-8 w-8" />
)}
</div>
<div>
<p className="font-semibold">{alert.title}</p>
{alert.message && <p className="mt-1 text-xs">{alert.message}</p>}
</div>
</div>
</div>
</div>
))}
</div>
);
};
export default ToastAlerts;

View File

@ -1,5 +1,13 @@
export const GeneralView = () => {
// const { instance, instanceAdmins } = useInstance();
import { observer } from "mobx-react-lite";
// hooks
import useInstance from "hooks/use-instance";
// ui
import { Loader } from "@plane/ui";
// components
import { InstanceGeneralForm } from "components/forms";
export const GeneralView = observer(() => {
const { instance, instanceAdmins } = useInstance();
return (
<div className="flex h-full w-full flex-col gap-8">
<div className="mb-2 border-b border-custom-border-100 pb-3">
@ -11,7 +19,7 @@ export const GeneralView = () => {
If you have a paid subscription, you will find your license key here.
</div>
</div>
{/* {instance && instanceAdmins ? (
{instance && instanceAdmins ? (
<InstanceGeneralForm
instance={instance}
instanceAdmins={instanceAdmins}
@ -24,7 +32,7 @@ export const GeneralView = () => {
</div>
<Loader.Item height="50px" />
</Loader>
)} */}
)}
</div>
);
};
});

View File

@ -4,9 +4,11 @@ import { InstanceContext } from "lib/instance-provider";
// types
import { IInstanceStore } from "store/instance.store";
export const useInstance = (): IInstanceStore => {
const useInstance = (): IInstanceStore => {
const context = useContext(InstanceContext);
if (context === undefined)
throw new Error("useTheme must be used within ThemeProvider");
throw new Error("useInstance must be used within InstanceProvider");
return context;
};
export default useInstance;

View File

@ -10,3 +10,5 @@ export const useAppTheme = (): IThemeStore => {
throw new Error("useTheme must be used within ThemeProvider");
return context;
};
export default useAppTheme;

View File

@ -0,0 +1,9 @@
import { useContext } from "react";
import { toastContext } from "lib/toast-provider";
const useToast = () => {
const toastContextData = useContext(toastContext);
return toastContextData;
};
export default useToast;

View File

@ -0,0 +1,14 @@
import { useContext } from "react";
// mobx store
import { UserContext } from "lib/user-provider";
// types
import { IUserStore } from "store/user.store";
const useUser = (): IUserStore => {
const context = useContext(UserContext);
if (context === undefined)
throw new Error("useUser must be used within UserProvider");
return context;
};
export default useUser;

View File

@ -0,0 +1,99 @@
"use client";
import React, { createContext, useCallback, useReducer } from "react";
// uuid
import { v4 as uuid } from "uuid";
// components
import ToastAlert from "components/toast-alert";
export const toastContext = createContext<ContextType>({} as ContextType);
// types
type ToastAlert = {
id: string;
title: string;
message?: string;
type: "success" | "error" | "warning" | "info";
};
type ReducerActionType = {
type: "SET_TOAST_ALERT" | "REMOVE_TOAST_ALERT";
payload: ToastAlert;
};
type ContextType = {
alerts?: ToastAlert[];
removeAlert: (id: string) => void;
setToastAlert: (data: {
title: string;
type?: "success" | "error" | "warning" | "info" | undefined;
message?: string | undefined;
}) => void;
};
type StateType = {
toastAlerts?: ToastAlert[];
};
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
export const initialState: StateType = {
toastAlerts: [],
};
export const reducer: ReducerFunctionType = (state, action) => {
const { type, payload } = action;
switch (type) {
case "SET_TOAST_ALERT":
return {
...state,
toastAlerts: [...(state.toastAlerts ?? []), payload],
};
case "REMOVE_TOAST_ALERT":
return {
...state,
toastAlerts: state.toastAlerts?.filter((toastAlert) => toastAlert.id !== payload.id),
};
default: {
return state;
}
}
};
export const ToastContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const removeAlert = useCallback((id: string) => {
dispatch({
type: "REMOVE_TOAST_ALERT",
payload: { id, title: "", message: "", type: "success" },
});
}, []);
const setToastAlert = useCallback(
(data: { title: string; type?: "success" | "error" | "warning" | "info"; message?: string }) => {
const id = uuid();
const { title, type, message } = data;
dispatch({
type: "SET_TOAST_ALERT",
payload: { id, title, message, type: type ?? "success" },
});
const timer = setTimeout(() => {
removeAlert(id);
clearTimeout(timer);
}, 3000);
},
[removeAlert]
);
return (
<toastContext.Provider value={{ setToastAlert, removeAlert, alerts: state.toastAlerts }}>
<ToastAlert />
{children}
</toastContext.Provider>
);
};

View File

@ -0,0 +1,29 @@
"use client";
import { createContext } from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ThemeProviderProps } from "next-themes/dist/types";
// mobx store
import { UserStore } from "store/user.store";
let userStore = new UserStore();
export const UserContext = createContext<UserStore>(userStore);
const initializeStore = () => {
const _userStore = userStore ?? new UserStore();
if (typeof window === "undefined") return _userStore;
if (!userStore) userStore = _userStore;
return _userStore;
};
export function UserProvider({ children, ...props }: ThemeProviderProps) {
const store = initializeStore();
return (
<>
<UserContext.Provider value={store}>
<NextThemesProvider {...props}>{children}</NextThemesProvider>
</UserContext.Provider>
</>
);
}

View File

@ -15,8 +15,10 @@
"@plane/ui": "*",
"@tailwindcss/typography": "^0.5.9",
"autoprefixer": "10.4.14",
"axios": "^1.6.7",
"eslint": "8.39.0",
"eslint-config-next": "13.3.1",
"js-cookie": "^3.0.5",
"mobx": "^6.12.0",
"mobx-react-lite": "^4.0.5",
"next": "^14.1.0",
@ -25,6 +27,7 @@
"postcss": "8.4.23",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"swr": "^2.2.4",
"tailwindcss": "3.3.2",
"typescript": "5.0.4"
},

View File

@ -0,0 +1,94 @@
import axios from "axios";
import Cookies from "js-cookie";
export abstract class APIService {
protected baseURL: string;
protected headers: any = {};
constructor(baseURL: string) {
this.baseURL = baseURL;
}
setRefreshToken(token: string) {
Cookies.set("refreshToken", token, { expires: 30 });
}
getRefreshToken() {
return Cookies.get("refreshToken");
}
purgeRefreshToken() {
Cookies.remove("refreshToken", { path: "/" });
}
setAccessToken(token: string) {
Cookies.set("accessToken", token, { expires: 30 });
}
getAccessToken() {
return Cookies.get("accessToken");
}
purgeAccessToken() {
Cookies.remove("accessToken", { path: "/" });
}
getHeaders() {
return {
Authorization: `Bearer ${this.getAccessToken()}`,
};
}
get(url: string, config = {}): Promise<any> {
return axios({
method: "get",
url: this.baseURL + url,
headers: this.getAccessToken() ? this.getHeaders() : {},
...config,
});
}
post(url: string, data = {}, config = {}): Promise<any> {
return axios({
method: "post",
url: this.baseURL + url,
data,
headers: this.getAccessToken() ? this.getHeaders() : {},
...config,
});
}
put(url: string, data = {}, config = {}): Promise<any> {
return axios({
method: "put",
url: this.baseURL + url,
data,
headers: this.getAccessToken() ? this.getHeaders() : {},
...config,
});
}
patch(url: string, data = {}, config = {}): Promise<any> {
return axios({
method: "patch",
url: this.baseURL + url,
data,
headers: this.getAccessToken() ? this.getHeaders() : {},
...config,
});
}
delete(url: string, data?: any, config = {}): Promise<any> {
return axios({
method: "delete",
url: this.baseURL + url,
data: data,
headers: this.getAccessToken() ? this.getHeaders() : {},
...config,
});
}
request(config = {}) {
return axios(config);
}
}

View File

@ -0,0 +1,148 @@
// services
import { APIService } from "services/api.service";
// types
import {
IEmailCheckData,
IEmailCheckResponse,
ILoginTokenResponse,
IMagicSignInData,
IPasswordSignInData,
} from "@plane/types";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : "";
export class AuthService extends APIService {
constructor() {
super(API_BASE_URL);
}
async emailCheck(data: IEmailCheckData): Promise<IEmailCheckResponse> {
return this.post("/api/email-check/", data, { headers: {} })
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async passwordSignIn(data: IPasswordSignInData): Promise<ILoginTokenResponse> {
return this.post("/api/sign-in/", data, { headers: {} })
.then((response) => {
this.setAccessToken(response?.data?.access_token);
this.setRefreshToken(response?.data?.refresh_token);
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
async sendResetPasswordLink(data: { email: string }): Promise<any> {
return this.post(`/api/forgot-password/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async setPassword(data: { password: string }): Promise<any> {
return this.post(`/api/users/me/set-password/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async resetPassword(
uidb64: string,
token: string,
data: {
new_password: string;
}
): Promise<ILoginTokenResponse> {
return this.post(`/api/reset-password/${uidb64}/${token}/`, data, { headers: {} })
.then((response) => {
if (response?.status === 200) {
this.setAccessToken(response?.data?.access_token);
this.setRefreshToken(response?.data?.refresh_token);
return response?.data;
}
})
.catch((error) => {
throw error?.response?.data;
});
}
async emailSignUp(data: { email: string; password: string }): Promise<ILoginTokenResponse> {
return this.post("/api/sign-up/", data, { headers: {} })
.then((response) => {
this.setAccessToken(response?.data?.access_token);
this.setRefreshToken(response?.data?.refresh_token);
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
async socialAuth(data: any): Promise<ILoginTokenResponse> {
return this.post("/api/social-auth/", data, { headers: {} })
.then((response) => {
this.setAccessToken(response?.data?.access_token);
this.setRefreshToken(response?.data?.refresh_token);
return response?.data;
})
.catch((error) => {
throw error?.response?.data;
});
}
async generateUniqueCode(data: { email: string }): Promise<any> {
return this.post("/api/magic-generate/", data, { headers: {} })
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async magicSignIn(data: IMagicSignInData): Promise<any> {
return await this.post("/api/magic-sign-in/", data, { headers: {} })
.then((response) => {
if (response?.status === 200) {
this.setAccessToken(response?.data?.access_token);
this.setRefreshToken(response?.data?.refresh_token);
return response?.data;
}
})
.catch((error) => {
throw error?.response?.data;
});
}
async instanceAdminSignIn(data: IPasswordSignInData): Promise<ILoginTokenResponse> {
return await this.post("/api/instances/admins/sign-in/", data, { headers: {} })
.then((response) => {
if (response?.status === 200) {
this.setAccessToken(response?.data?.access_token);
this.setRefreshToken(response?.data?.refresh_token);
return response?.data;
}
})
.catch((error) => {
throw error?.response?.data;
});
}
async signOut(): Promise<any> {
return this.post("/api/sign-out/", { refresh_token: this.getRefreshToken() })
.then((response) => {
this.purgeAccessToken();
this.purgeRefreshToken();
return response?.data;
})
.catch((error) => {
this.purgeAccessToken();
this.purgeRefreshToken();
throw error?.response?.data;
});
}
}

View File

@ -0,0 +1,53 @@
import { APIService } from "services/api.service";
// types
import type { IFormattedInstanceConfiguration, IInstance, IInstanceAdmin, IInstanceConfiguration } from "@plane/types";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : "";
export class InstanceService extends APIService {
constructor() {
super(API_BASE_URL);
}
async getInstanceInfo(): Promise<IInstance> {
return this.get("/api/instances/", { headers: {} })
.then((response) => response.data)
.catch((error) => {
throw error;
});
}
async getInstanceAdmins(): Promise<IInstanceAdmin[]> {
return this.get("/api/instances/admins/")
.then((response) => response.data)
.catch((error) => {
throw error;
});
}
async updateInstanceInfo(data: Partial<IInstance>): Promise<IInstance> {
return this.patch("/api/instances/", data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async getInstanceConfigurations() {
return this.get("/api/instances/configurations/")
.then((response) => response.data)
.catch((error) => {
throw error;
});
}
async updateInstanceConfigurations(
data: Partial<IFormattedInstanceConfiguration>
): Promise<IInstanceConfiguration[]> {
return this.patch("/api/instances/configurations/", data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}

View File

@ -0,0 +1,37 @@
// services
import { APIService } from "services/api.service";
// types
import type { IUser, IInstanceAdminStatus } from "@plane/types";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL
? process.env.NEXT_PUBLIC_API_BASE_URL
: "";
export class UserService extends APIService {
constructor() {
super(API_BASE_URL);
}
currentUserConfig() {
return {
url: `${this.baseURL}/api/users/me/`,
headers: this.getHeaders(),
};
}
async currentUser(): Promise<IUser> {
return this.get("/api/users/me/")
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async currentUserInstanceAdminStatus(): Promise<IInstanceAdminStatus> {
return this.get("/api/users/me/instance-admin/")
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
}

View File

@ -1,14 +1,149 @@
// mobx
import { action, observable, makeObservable } from "mobx";
import {
observable,
action,
computed,
makeObservable,
runInAction,
} from "mobx";
// types
import {
IInstance,
IInstanceConfiguration,
IFormattedInstanceConfiguration,
IInstanceAdmin,
} from "@plane/types";
// services
import { InstanceService } from "services/instance.service";
export interface IInstanceStore {}
export interface IInstanceStore {
// issues
instance: IInstance | null;
instanceAdmins: IInstanceAdmin[] | null;
configurations: IInstanceConfiguration[] | null;
// computed
formattedConfig: IFormattedInstanceConfiguration | null;
// action
fetchInstanceInfo: () => Promise<IInstance>;
fetchInstanceAdmins: () => Promise<IInstanceAdmin[]>;
updateInstanceInfo: (data: Partial<IInstance>) => Promise<IInstance>;
fetchInstanceConfigurations: () => Promise<any>;
updateInstanceConfigurations: (
data: Partial<IFormattedInstanceConfiguration>
) => Promise<IInstanceConfiguration[]>;
}
export class InstanceStore implements IInstanceStore {
instance: IInstance | null = null;
instanceAdmins: IInstanceAdmin[] | null = null;
configurations: IInstanceConfiguration[] | null = null;
// service
instanceService;
constructor() {
makeObservable(this, {
// observable
// action
instance: observable,
instanceAdmins: observable,
configurations: observable,
// computed
formattedConfig: computed,
// actions
fetchInstanceInfo: action,
fetchInstanceAdmins: action,
updateInstanceInfo: action,
fetchInstanceConfigurations: action,
updateInstanceConfigurations: action,
});
this.instanceService = new InstanceService();
}
/**
* computed value for instance configurations data for forms.
* @returns configurations in the form of {key, value} pair.
*/
get formattedConfig() {
if (!this.configurations) return null;
return this.configurations?.reduce(
(formData: IFormattedInstanceConfiguration, config) => {
formData[config.key] = config.value;
return formData;
},
{}
);
}
/**
* fetch instance info from API
*/
fetchInstanceInfo = async () => {
try {
const instance = await this.instanceService.getInstanceInfo();
runInAction(() => {
this.instance = instance;
});
return instance;
} catch (error) {
console.log("Error while fetching the instance info");
throw error;
}
};
/**
* fetch instance admins from API
*/
fetchInstanceAdmins = async () => {
const instanceAdmins = await this.instanceService.getInstanceAdmins();
runInAction(() => {
this.instanceAdmins = instanceAdmins;
});
return instanceAdmins;
};
/**
* update instance info
* @param data
*/
updateInstanceInfo = async (data: Partial<IInstance>) =>
await this.instanceService.updateInstanceInfo(data).then((response) => {
runInAction(() => {
this.instance = response;
});
return response;
});
/**
* fetch instance configurations from API
*/
fetchInstanceConfigurations = async () => {
try {
const configurations =
await this.instanceService.getInstanceConfigurations();
runInAction(() => {
this.configurations = configurations;
});
return configurations;
} catch (error) {
console.log("Error while fetching the instance configurations");
throw error;
}
};
/**
* update instance configurations
* @param data
*/
updateInstanceConfigurations = async (
data: Partial<IFormattedInstanceConfiguration>
) =>
await this.instanceService
.updateInstanceConfigurations(data)
.then((response) => {
runInAction(() => {
this.configurations = this.configurations
? [...this.configurations, ...response]
: response;
});
return response;
});
}

View File

@ -0,0 +1,100 @@
import { action, observable, runInAction, makeObservable } from "mobx";
// services
import { UserService } from "services/user.service";
import { AuthService } from "services/auth.service";
// interfaces
import { IUser } from "@plane/types";
export interface IUserStore {
// states
currentUserError: any | null;
currentUserLoader: boolean;
// observables
isUserLoggedIn: boolean | null;
currentUser: IUser | null;
isUserInstanceAdmin: boolean | null;
// fetch actions
fetchCurrentUser: () => Promise<IUser>;
fetchCurrentUserInstanceAdminStatus: () => Promise<boolean>;
signOut: () => Promise<void>;
}
export class UserStore implements IUserStore {
// states
currentUserError: any | null = null;
currentUserLoader: boolean = false;
// observables
isUserLoggedIn: boolean | null = null;
currentUser: IUser | null = null;
isUserInstanceAdmin: boolean | null = null;
// services
userService;
authService;
constructor() {
makeObservable(this, {
// states
currentUserError: observable.ref,
currentUserLoader: observable.ref,
// observable
currentUser: observable,
isUserInstanceAdmin: observable.ref,
// action
fetchCurrentUser: action,
fetchCurrentUserInstanceAdminStatus: action,
signOut: action,
});
this.userService = new UserService();
this.authService = new AuthService();
}
/**
* Fetches the current user
* @returns Promise<IUser>
*/
fetchCurrentUser = async () => {
try {
this.currentUserLoader = true;
const response = await this.userService.currentUser();
runInAction(() => {
this.isUserLoggedIn = true;
this.currentUser = response;
this.currentUserError = null;
this.currentUserLoader = false;
});
return response;
} catch (error) {
runInAction(() => {
this.currentUserLoader = false;
this.currentUserError = error;
});
throw error;
}
};
/**
* Fetches the current user instance admin status
* @returns Promise<boolean>
*/
fetchCurrentUserInstanceAdminStatus = async () =>
await this.userService.currentUserInstanceAdminStatus().then((response) => {
runInAction(() => {
this.isUserInstanceAdmin = response.is_instance_admin;
});
return response.is_instance_admin;
});
/**
* Signs out the current user
* @returns Promise<void>
*/
signOut = async () =>
await this.authService.signOut().then(() => {
runInAction(() => {
this.currentUser = null;
this.isUserLoggedIn = false;
});
});
}

View File

@ -10,6 +10,7 @@ module.exports = {
"./constants/**/*.{js,ts,jsx,tsx}",
"./layouts/**/*.tsx",
"./pages/**/*.tsx",
"./app/**/*.tsx",
"./ui/**/*.tsx",
"../packages/ui/**/*.{js,ts,jsx,tsx}",
"../packages/editor/**/src/**/*.{js,ts,jsx,tsx}",

View File

@ -3422,6 +3422,15 @@ axios@^1.1.3, axios@^1.3.4:
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axios@^1.6.7:
version "1.6.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7"
integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==
dependencies:
follow-redirects "^1.15.4"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axobject-query@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a"
@ -5271,6 +5280,11 @@ follow-redirects@^1.15.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
follow-redirects@^1.15.4:
version "1.15.5"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020"
integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==
for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@ -6065,7 +6079,7 @@ joycon@^3.0.1:
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
js-cookie@^3.0.1:
js-cookie@^3.0.1, js-cookie@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
@ -8625,7 +8639,7 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
swr@^2.1.3, swr@^2.2.2:
swr@^2.1.3, swr@^2.2.2, swr@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07"
integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==