mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'chore-admin-file-structure' of github.com:makeplane/plane into chore-admin-file-structure
This commit is contained in:
commit
7d63e6ad25
@ -96,7 +96,7 @@ export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
|
||||
<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>
|
||||
<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="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) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { FC, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import React, { FC, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
|
||||
import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { ControllerInput, TControllerInputFormField } from "components/common";
|
||||
import { SendTestEmailModal } from "./test-email-modal";
|
||||
@ -16,6 +16,14 @@ type IInstanceEmailForm = {
|
||||
|
||||
type EmailFormValues = Record<TInstanceEmailConfigurationKeys, string>;
|
||||
|
||||
type TEmailSecurityKeys = "EMAIL_USE_TLS" | "EMAIL_USE_SSL" | "NONE";
|
||||
|
||||
const EMAIL_SECURITY_OPTIONS: { [key in TEmailSecurityKeys]: string } = {
|
||||
EMAIL_USE_TLS: "TLS",
|
||||
EMAIL_USE_SSL: "SSL",
|
||||
NONE: "No email security",
|
||||
};
|
||||
|
||||
export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
const { config } = props;
|
||||
// states
|
||||
@ -26,8 +34,9 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
formState: { errors, isValid, isDirty, isSubmitting },
|
||||
} = useForm<EmailFormValues>({
|
||||
defaultValues: {
|
||||
EMAIL_HOST: config["EMAIL_HOST"],
|
||||
@ -35,7 +44,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
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_USE_SSL: config["EMAIL_USE_SSL"],
|
||||
EMAIL_FROM: config["EMAIL_FROM"],
|
||||
},
|
||||
});
|
||||
@ -57,13 +66,26 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
error: Boolean(errors.EMAIL_PORT),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "EMAIL_FROM",
|
||||
type: "text",
|
||||
label: "Sender email address",
|
||||
description:
|
||||
"This is the email address your users will see when getting emails from this instance. You will need to verify this address.",
|
||||
placeholder: "no-reply@projectplane.so",
|
||||
error: Boolean(errors.EMAIL_FROM),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
const OptionalEmailFormFields: TControllerInputFormField[] = [
|
||||
{
|
||||
key: "EMAIL_HOST_USER",
|
||||
type: "text",
|
||||
label: "Username",
|
||||
placeholder: "getitdone@projectplane.so",
|
||||
error: Boolean(errors.EMAIL_HOST_USER),
|
||||
required: true,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "EMAIL_HOST_PASSWORD",
|
||||
@ -71,17 +93,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
label: "Password",
|
||||
placeholder: "Password",
|
||||
error: Boolean(errors.EMAIL_HOST_PASSWORD),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "EMAIL_FROM",
|
||||
type: "text",
|
||||
label: "From address",
|
||||
description:
|
||||
"This is the email address your users will see when getting emails from this instance. You will need to verify this address.",
|
||||
placeholder: "no-reply@projectplane.so",
|
||||
error: Boolean(errors.EMAIL_FROM),
|
||||
required: true,
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
@ -99,11 +111,34 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
.catch((err) => console.error(err));
|
||||
};
|
||||
|
||||
const useTLSValue = watch("EMAIL_USE_TLS");
|
||||
const useSSLValue = watch("EMAIL_USE_SSL");
|
||||
const emailSecurityKey: TEmailSecurityKeys = useMemo(() => {
|
||||
if (useTLSValue === "1") return "EMAIL_USE_TLS";
|
||||
if (useSSLValue === "1") return "EMAIL_USE_SSL";
|
||||
return "NONE";
|
||||
}, [useTLSValue, useSSLValue]);
|
||||
|
||||
const handleEmailSecurityChange = (key: TEmailSecurityKeys) => {
|
||||
if (key === "EMAIL_USE_SSL") {
|
||||
setValue("EMAIL_USE_TLS", "0");
|
||||
setValue("EMAIL_USE_SSL", "1");
|
||||
}
|
||||
if (key === "EMAIL_USE_TLS") {
|
||||
setValue("EMAIL_USE_TLS", "1");
|
||||
setValue("EMAIL_USE_SSL", "0");
|
||||
}
|
||||
if (key === "NONE") {
|
||||
setValue("EMAIL_USE_TLS", "0");
|
||||
setValue("EMAIL_USE_SSL", "0");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<SendTestEmailModal isOpen={isSendTestEmailModalOpen} handleClose={() => setIsSendTestEmailModalOpen(false)} />
|
||||
<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="grid-col grid w-full max-w-4xl grid-cols-1 items-start justify-between gap-10 lg:grid-cols-2">
|
||||
{emailFormFields.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
@ -117,41 +152,67 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
required={field.required}
|
||||
/>
|
||||
))}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">Email security</h4>
|
||||
<CustomSelect
|
||||
value={emailSecurityKey}
|
||||
label={EMAIL_SECURITY_OPTIONS[emailSecurityKey]}
|
||||
onChange={handleEmailSecurityChange}
|
||||
buttonClassName="rounded-md border-custom-border-200"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{Object.entries(EMAIL_SECURITY_OPTIONS).map(([key, value]) => (
|
||||
<CustomSelect.Option key={key} value={key} className="w-full">
|
||||
{value}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-custom-border-100">
|
||||
<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-sm font-medium text-custom-text-100">Authentication (optional)</div>
|
||||
<div className="text-xs font-normal text-custom-text-300">
|
||||
Use this if your email domain supports TLS.
|
||||
We recommend setting up a username password for your SMTP server
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<Controller
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-center justify-between gap-10 lg:grid-cols-2">
|
||||
{OptionalEmailFormFields.map((field) => (
|
||||
<ControllerInput
|
||||
key={field.key}
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
type={field.type}
|
||||
name={field.key}
|
||||
label={field.label}
|
||||
description={field.description}
|
||||
placeholder={field.placeholder}
|
||||
error={field.error}
|
||||
required={field.required}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex max-w-4xl items-center py-1 gap-4">
|
||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
loading={isSubmitting}
|
||||
disabled={!isValid || !isDirty}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
<Button variant="outline-primary" onClick={() => setIsSendTestEmailModalOpen(true)} loading={isSubmitting}>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={() => setIsSendTestEmailModalOpen(true)}
|
||||
loading={isSubmitting}
|
||||
disabled={!isValid}
|
||||
>
|
||||
Send test email
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import { FC, useState, useRef } from "react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import Link from "next/link";
|
||||
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { DiscordIcon, GithubIcon } from "@plane/ui";
|
||||
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks";
|
||||
// assets
|
||||
@ -45,6 +45,7 @@ export const HelpSection: FC = () => {
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
|
||||
<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`}
|
||||
@ -52,7 +53,8 @@ export const HelpSection: FC = () => {
|
||||
<ExternalLink size={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 ${
|
||||
@ -62,22 +64,18 @@ export const HelpSection: FC = () => {
|
||||
>
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
</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 md:hidden"
|
||||
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
||||
>
|
||||
<MoveLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${
|
||||
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 ${
|
||||
isSidebarCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
||||
>
|
||||
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isSidebarCollapsed ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
|
@ -9,7 +9,7 @@ import { Avatar } from "@plane/ui";
|
||||
// hooks
|
||||
import { useTheme, useUser } from "@/hooks";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { API_BASE_URL, cn } from "@/helpers/common.helper";
|
||||
// services
|
||||
import { AuthService } from "@/services";
|
||||
|
||||
@ -32,6 +32,45 @@ export const SidebarDropdown = observer(() => {
|
||||
|
||||
const handleSignOut = () => signOut();
|
||||
|
||||
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",
|
||||
{
|
||||
"left-4": isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-2.5 pb-2">
|
||||
<span className="px-2 text-custom-sidebar-text-200">{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"
|
||||
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">
|
||||
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`} onSubmit={handleSignOut}>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<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"
|
||||
>
|
||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||
Sign out
|
||||
</Menu.Item>
|
||||
</form>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (csrfToken === undefined)
|
||||
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||
@ -44,10 +83,31 @@ export const SidebarDropdown = observer(() => {
|
||||
className={`flex flex-grow items-center gap-x-2 truncate rounded py-1 ${
|
||||
isSidebarCollapsed ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<Menu as="div" className="flex-shrink-0">
|
||||
<Menu.Button
|
||||
className={cn("grid place-items-center outline-none", {
|
||||
"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>
|
||||
</Menu.Button>
|
||||
{isSidebarCollapsed && (
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
{getSidebarMenuItems()}
|
||||
</Transition>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
{!isSidebarCollapsed && (
|
||||
<div className="flex w-full gap-2">
|
||||
@ -78,38 +138,7 @@ export const SidebarDropdown = observer(() => {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="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"
|
||||
>
|
||||
<div className="flex flex-col gap-2.5 pb-2">
|
||||
<span className="px-2 text-custom-sidebar-text-200">{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"
|
||||
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">
|
||||
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`} onSubmit={handleSignOut}>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<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"
|
||||
>
|
||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||
Sign out
|
||||
</Menu.Item>
|
||||
</form>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
{getSidebarMenuItems()}
|
||||
</Transition>
|
||||
</Menu>
|
||||
)}
|
||||
|
@ -6,6 +6,8 @@ import { Controller, Control } from "react-hook-form";
|
||||
import { Input } from "@plane/ui";
|
||||
// icons
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
type Props = {
|
||||
control: Control<any>;
|
||||
@ -51,7 +53,9 @@ export const ControllerInput: React.FC<Props> = (props) => {
|
||||
ref={ref}
|
||||
hasError={error}
|
||||
placeholder={placeholder}
|
||||
className="w-full rounded-md font-medium"
|
||||
className={cn("w-full rounded-md font-medium", {
|
||||
"pr-10": type === "password",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -72,7 +76,7 @@ export const ControllerInput: React.FC<Props> = (props) => {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{description && <p className="text-xs text-custom-text-400">{description}</p>}
|
||||
{description && <p className="text-xs text-custom-text-300">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -11,6 +11,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
|
@ -111,6 +111,7 @@ class SignInAuthEndpoint(View):
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = {
|
||||
"email": email,
|
||||
"error_code": str(e.error_code),
|
||||
"error_message": str(e.error_message),
|
||||
}
|
||||
|
@ -333,7 +333,7 @@ SESSION_SAVE_EVERY_REQUEST = True
|
||||
|
||||
# Admin Cookie
|
||||
ADMIN_SESSION_COOKIE_NAME = "plane-admin-session-id"
|
||||
ADMIN_SESSION_COOKIE_AGE = 18000
|
||||
ADMIN_SESSION_COOKIE_AGE = 3600
|
||||
|
||||
# CSRF cookies
|
||||
CSRF_COOKIE_SECURE = secure_origins
|
||||
|
20
packages/types/src/users.d.ts
vendored
20
packages/types/src/users.d.ts
vendored
@ -27,7 +27,7 @@ export interface IUser {
|
||||
user_timezone: string;
|
||||
username: string;
|
||||
last_login_medium: TLoginMediums;
|
||||
// theme: IUserTheme;
|
||||
theme: IUserTheme;
|
||||
}
|
||||
|
||||
export interface IUserAccount {
|
||||
@ -48,7 +48,7 @@ export type TUserProfile = {
|
||||
palette: string | undefined;
|
||||
primary: string | undefined;
|
||||
background: string | undefined;
|
||||
darkPalette: string | undefined;
|
||||
darkPalette: boolean | undefined;
|
||||
sidebarText: string | undefined;
|
||||
sidebarBackground: string | undefined;
|
||||
};
|
||||
@ -80,14 +80,14 @@ export interface IUserSettings {
|
||||
}
|
||||
|
||||
export interface IUserTheme {
|
||||
background: string;
|
||||
text: string;
|
||||
primary: string;
|
||||
sidebarBackground: string;
|
||||
sidebarText: string;
|
||||
darkPalette: boolean;
|
||||
palette: string;
|
||||
theme: string;
|
||||
text: string | undefined;
|
||||
theme: string | undefined;
|
||||
palette: string | undefined;
|
||||
primary: string | undefined;
|
||||
background: string | undefined;
|
||||
darkPalette: boolean | undefined;
|
||||
sidebarText: string | undefined;
|
||||
sidebarBackground: string | undefined;
|
||||
}
|
||||
|
||||
export interface IUserLite {
|
||||
|
@ -67,7 +67,7 @@ const CustomSelect = (props: ICustomSelectProps) => {
|
||||
className={`flex items-center justify-between gap-1 text-xs ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${customButtonClassName}`}
|
||||
onClick={openDropdown}
|
||||
onClick={isOpen ? closeDropdown : openDropdown}
|
||||
>
|
||||
{customButton}
|
||||
</button>
|
||||
@ -77,12 +77,17 @@ const CustomSelect = (props: ICustomSelectProps) => {
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 ${
|
||||
input ? "px-3 py-2 text-sm" : "px-2 py-1 text-xs"
|
||||
} ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={openDropdown}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300",
|
||||
{
|
||||
"px-3 py-2 text-sm": input,
|
||||
"px-2 py-1 text-xs": !input,
|
||||
"cursor-not-allowed text-custom-text-200": disabled,
|
||||
"cursor-pointer hover:bg-custom-background-80": !disabled,
|
||||
},
|
||||
buttonClassName
|
||||
)}
|
||||
onClick={isOpen ? closeDropdown : openDropdown}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
|
@ -48,7 +48,15 @@ export const SignInAuthRoot = observer(() => {
|
||||
error_code?.toString() as EAuthenticationErrorCodes,
|
||||
error_message?.toString()
|
||||
);
|
||||
if (errorhandler) setErrorInfo(errorhandler);
|
||||
if (errorhandler) {
|
||||
if (errorhandler?.type === EErrorAlertType.TOAST_ALERT) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: errorhandler?.title,
|
||||
message: errorhandler?.message as string,
|
||||
});
|
||||
} else setErrorInfo(errorhandler);
|
||||
}
|
||||
}
|
||||
}, [error_code, error_message]);
|
||||
|
||||
|
@ -52,7 +52,15 @@ export const SignUpAuthRoot: FC = observer(() => {
|
||||
error_code?.toString() as EAuthenticationErrorCodes,
|
||||
error_message?.toString()
|
||||
);
|
||||
if (errorhandler) setErrorInfo(errorhandler);
|
||||
if (errorhandler) {
|
||||
if (errorhandler?.type === EErrorAlertType.TOAST_ALERT) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: errorhandler?.title,
|
||||
message: errorhandler?.message as string,
|
||||
});
|
||||
} else setErrorInfo(errorhandler);
|
||||
}
|
||||
}
|
||||
}, [error_code, error_message]);
|
||||
|
||||
|
@ -2,10 +2,12 @@ import React, { FC, useEffect, useState } from "react";
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
// icons
|
||||
import { Settings } from "lucide-react";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import { THEME_OPTIONS } from "@/constants/themes";
|
||||
// hooks
|
||||
import { useUserProfile } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
@ -13,20 +15,20 @@ type Props = {
|
||||
|
||||
export const CommandPaletteThemeActions: FC<Props> = observer((props) => {
|
||||
const { closePalette } = props;
|
||||
const { setTheme } = useTheme();
|
||||
// hooks
|
||||
const { updateUserTheme } = useUserProfile();
|
||||
// states
|
||||
const [mounted, setMounted] = useState(false);
|
||||
// hooks
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const updateUserTheme = async (newTheme: string) => {
|
||||
const updateTheme = async (newTheme: string) => {
|
||||
setTheme(newTheme);
|
||||
|
||||
// return updateUserProfile({ theme: newTheme }).catch(() => {
|
||||
// setToast({
|
||||
// type: TOAST_TYPE.ERROR,
|
||||
// title: "Failed to save user theme settings!",
|
||||
// });
|
||||
// });
|
||||
return updateUserTheme({ theme: newTheme }).catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Failed to save user theme settings!",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// useEffect only runs on the client, so now we can safely show the UI
|
||||
@ -42,7 +44,7 @@ export const CommandPaletteThemeActions: FC<Props> = observer((props) => {
|
||||
<Command.Item
|
||||
key={theme.value}
|
||||
onSelect={() => {
|
||||
updateUserTheme(theme.value);
|
||||
updateTheme(theme.value);
|
||||
closePalette();
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
|
@ -4,9 +4,9 @@ import { Controller, useForm } from "react-hook-form";
|
||||
// types
|
||||
import { IUserTheme } from "@plane/types";
|
||||
// ui
|
||||
import { Button, InputColorPicker } from "@plane/ui";
|
||||
import { Button, InputColorPicker, setPromiseToast } from "@plane/ui";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useUserProfile } from "@/hooks/store";
|
||||
|
||||
const inputRules = {
|
||||
required: "Background color is required",
|
||||
@ -25,13 +25,9 @@ const inputRules = {
|
||||
};
|
||||
|
||||
export const CustomThemeSelector: React.FC = observer(() => {
|
||||
const {
|
||||
userProfile: { data: userProfile },
|
||||
} = useUser();
|
||||
|
||||
const userTheme: any = userProfile?.theme;
|
||||
// hooks
|
||||
const { setTheme } = useTheme();
|
||||
// hooks
|
||||
const { data: userProfile, updateUserTheme } = useUserProfile();
|
||||
|
||||
const {
|
||||
control,
|
||||
@ -40,17 +36,18 @@ export const CustomThemeSelector: React.FC = observer(() => {
|
||||
watch,
|
||||
} = useForm<IUserTheme>({
|
||||
defaultValues: {
|
||||
background: userTheme?.background !== "" ? userTheme?.background : "#0d101b",
|
||||
text: userTheme?.text !== "" ? userTheme?.text : "#c5c5c5",
|
||||
primary: userTheme?.primary !== "" ? userTheme?.primary : "#3f76ff",
|
||||
sidebarBackground: userTheme?.sidebarBackground !== "" ? userTheme?.sidebarBackground : "#0d101b",
|
||||
sidebarText: userTheme?.sidebarText !== "" ? userTheme?.sidebarText : "#c5c5c5",
|
||||
darkPalette: userTheme?.darkPalette || false,
|
||||
palette: userTheme?.palette !== "" ? userTheme?.palette : "",
|
||||
background: userProfile?.theme?.background !== "" ? userProfile?.theme?.background : "#0d101b",
|
||||
text: userProfile?.theme?.text !== "" ? userProfile?.theme?.text : "#c5c5c5",
|
||||
primary: userProfile?.theme?.primary !== "" ? userProfile?.theme?.primary : "#3f76ff",
|
||||
sidebarBackground:
|
||||
userProfile?.theme?.sidebarBackground !== "" ? userProfile?.theme?.sidebarBackground : "#0d101b",
|
||||
sidebarText: userProfile?.theme?.sidebarText !== "" ? userProfile?.theme?.sidebarText : "#c5c5c5",
|
||||
darkPalette: userProfile?.theme?.darkPalette || false,
|
||||
palette: userProfile?.theme?.palette !== "" ? userProfile?.theme?.palette : "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleUpdateTheme = async (formData: any) => {
|
||||
const handleUpdateTheme = async (formData: Partial<IUserTheme>) => {
|
||||
const payload: IUserTheme = {
|
||||
background: formData.background,
|
||||
text: formData.text,
|
||||
@ -61,12 +58,22 @@ export const CustomThemeSelector: React.FC = observer(() => {
|
||||
palette: `${formData.background},${formData.text},${formData.primary},${formData.sidebarBackground},${formData.sidebarText}`,
|
||||
theme: "custom",
|
||||
};
|
||||
|
||||
setTheme("custom");
|
||||
|
||||
console.log(payload);
|
||||
const updateCurrentUserThemePromise = updateUserTheme(payload);
|
||||
setPromiseToast(updateCurrentUserThemePromise, {
|
||||
loading: "Updating theme...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Theme updated successfully!",
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Failed to Update the theme",
|
||||
},
|
||||
});
|
||||
|
||||
// return updateUserProfile({ theme: payload });
|
||||
return;
|
||||
};
|
||||
|
||||
const handleValueChange = (val: string | undefined, onChange: any) => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export enum EPageTypes {
|
||||
"PUBLIC" = "PUBLIC",
|
||||
"NON_AUTHENTICATED" = "NON_AUTHENTICATED",
|
||||
@ -35,11 +37,6 @@ export enum EAuthenticationErrorCodes {
|
||||
REQUIRED_EMAIL_PASSWORD_FIRST_NAME = "REQUIRED_EMAIL_PASSWORD_FIRST_NAME",
|
||||
REQUIRED_EMAIL_PASSWORD = "REQUIRED_EMAIL_PASSWORD",
|
||||
EMAIL_CODE_REQUIRED = "EMAIL_CODE_REQUIRED",
|
||||
// inline local errors
|
||||
INLINE_EMAIL = "INLINE_EMAIL",
|
||||
INLINE_PASSWORD = "INLINE_PASSWORD",
|
||||
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
|
||||
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
|
||||
}
|
||||
|
||||
export enum EErrorAlertType {
|
||||
@ -51,7 +48,64 @@ export enum EErrorAlertType {
|
||||
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
|
||||
}
|
||||
|
||||
export type TAuthErrorInfo = { type: EErrorAlertType; message: string };
|
||||
export type TAuthErrorInfo = { type: EErrorAlertType; title: string; message: ReactNode };
|
||||
|
||||
const errorCodeMessages: { [key in EAuthenticationErrorCodes]: { title: string; message: ReactNode } } = {
|
||||
[EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED]: {
|
||||
title: `Instance not configured`,
|
||||
message: `Instance not configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.SMTP_NOT_CONFIGURED]: {
|
||||
title: `SMTP not configured`,
|
||||
message: `SMTP not configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.AUTHENTICATION_FAILED]: {
|
||||
title: `Authentication failed.`,
|
||||
message: `Authentication failed. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_TOKEN]: { title: `Invalid token.`, message: `Invalid token. Please try again.` },
|
||||
[EAuthenticationErrorCodes.EXPIRED_TOKEN]: { title: `Expired token.`, message: `Expired token. Please try again.` },
|
||||
[EAuthenticationErrorCodes.IMPROPERLY_CONFIGURED]: {
|
||||
title: `Improperly configured.`,
|
||||
message: `Improperly configured. Please contact your administrator.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.OAUTH_PROVIDER_ERROR]: {
|
||||
title: `OAuth provider error.`,
|
||||
message: `OAuth provider error. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_EMAIL]: {
|
||||
title: `Invalid email.`,
|
||||
message: `Invalid email. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_PASSWORD]: {
|
||||
title: `Invalid password.`,
|
||||
message: `Invalid password. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.USER_DOES_NOT_EXIST]: {
|
||||
title: `User does not exist.`,
|
||||
message: `User does not exist. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: {
|
||||
title: `Admin already exists.`,
|
||||
message: `Admin already exists. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.USER_ALREADY_EXIST]: {
|
||||
title: `User already exists.`,
|
||||
message: `User already exists. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_FIRST_NAME]: {
|
||||
title: `Missing fields.`,
|
||||
message: `Email, password, and first name are required.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD]: {
|
||||
title: `Missing fields.`,
|
||||
message: `Email and password are required.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.EMAIL_CODE_REQUIRED]: {
|
||||
title: `Missing fields.`,
|
||||
message: `Email and code are required.`,
|
||||
},
|
||||
};
|
||||
|
||||
export const authErrorHandler = (
|
||||
errorCode: EAuthenticationErrorCodes,
|
||||
@ -67,49 +121,28 @@ export const authErrorHandler = (
|
||||
EAuthenticationErrorCodes.OAUTH_PROVIDER_ERROR,
|
||||
];
|
||||
const bannerAlertErrorCodes = [
|
||||
EAuthenticationErrorCodes.INVALID_EMAIL,
|
||||
EAuthenticationErrorCodes.INVALID_PASSWORD,
|
||||
EAuthenticationErrorCodes.USER_DOES_NOT_EXIST,
|
||||
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
|
||||
EAuthenticationErrorCodes.USER_ALREADY_EXIST,
|
||||
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD_FIRST_NAME,
|
||||
EAuthenticationErrorCodes.REQUIRED_EMAIL_PASSWORD,
|
||||
EAuthenticationErrorCodes.EMAIL_CODE_REQUIRED,
|
||||
];
|
||||
const inlineFirstNameErrorCodes = [EAuthenticationErrorCodes.INLINE_FIRST_NAME];
|
||||
const inlineEmailErrorCodes = [EAuthenticationErrorCodes.INLINE_EMAIL];
|
||||
const inlineEmailCodeErrorCodes = [EAuthenticationErrorCodes.INLINE_EMAIL_CODE];
|
||||
const inlinePasswordErrorCodes = [EAuthenticationErrorCodes.INLINE_PASSWORD];
|
||||
|
||||
if (toastAlertErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.TOAST_ALERT,
|
||||
message: errorMessage || "Something went wrong. Please try again.",
|
||||
title: errorCodeMessages[errorCode]?.title || "Error",
|
||||
message: errorMessage || errorCodeMessages[errorCode]?.message || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
if (bannerAlertErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.BANNER_ALERT,
|
||||
message: errorMessage || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
if (inlineFirstNameErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.INLINE_FIRST_NAME,
|
||||
message: errorMessage || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
if (inlineEmailErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.INLINE_EMAIL,
|
||||
message: errorMessage || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
if (inlinePasswordErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.INLINE_PASSWORD,
|
||||
message: errorMessage || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
if (inlineEmailCodeErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.INLINE_EMAIL_CODE,
|
||||
message: errorMessage || "Something went wrong. Please try again.",
|
||||
title: errorCodeMessages[errorCode]?.title || "Error",
|
||||
message: errorMessage || errorCodeMessages[errorCode]?.message || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
return undefined;
|
||||
|
@ -2,8 +2,6 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTheme } from "next-themes";
|
||||
import { mutate } from "swr";
|
||||
// icons
|
||||
import { ChevronLeft, LogOut, MoveLeft, Plus, UserPlus } from "lucide-react";
|
||||
// ui
|
||||
@ -11,7 +9,7 @@ import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import { PROFILE_ACTION_LINKS } from "@/constants/profile";
|
||||
// hooks
|
||||
import { useAppTheme, useUser, useWorkspace } from "@/hooks/store";
|
||||
import { useAppTheme, useUser, useUserSettings, useWorkspace } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
@ -35,22 +33,19 @@ export const ProfileLayoutSidebar = observer(() => {
|
||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
// next themes
|
||||
const { setTheme } = useTheme();
|
||||
// store hooks
|
||||
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
|
||||
const { data: currentUser, signOut } = useUser();
|
||||
// const { currentUserSettings } = useUser();
|
||||
const { data: currentUserSettings } = useUserSettings();
|
||||
const { workspaces } = useWorkspace();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const workspacesList = Object.values(workspaces ?? {});
|
||||
|
||||
// redirect url for normal mode
|
||||
// FIXME:
|
||||
const redirectWorkspaceSlug =
|
||||
// currentUserSettings?.workspace?.last_workspace_slug ||
|
||||
// currentUserSettings?.workspace?.fallback_workspace_slug ||
|
||||
currentUserSettings?.workspace?.last_workspace_slug ||
|
||||
currentUserSettings?.workspace?.fallback_workspace_slug ||
|
||||
"";
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
@ -5,7 +5,7 @@ import { useTheme } from "next-themes";
|
||||
// helpers
|
||||
import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper";
|
||||
// hooks
|
||||
import { useAppRouter, useAppTheme, useUser } from "@/hooks/store";
|
||||
import { useAppRouter, useAppTheme, useUserProfile } from "@/hooks/store";
|
||||
|
||||
type TStoreWrapper = {
|
||||
children: ReactNode;
|
||||
@ -20,9 +20,7 @@ const StoreWrapper: FC<TStoreWrapper> = observer((props) => {
|
||||
// store hooks
|
||||
const { setQuery } = useAppRouter();
|
||||
const { sidebarCollapsed, toggleSidebar } = useAppTheme();
|
||||
const {
|
||||
userProfile: { data: userProfile },
|
||||
} = useUser();
|
||||
const { data: userProfile } = useUserProfile();
|
||||
// states
|
||||
const [dom, setDom] = useState<undefined | HTMLElement>();
|
||||
|
||||
@ -40,14 +38,19 @@ const StoreWrapper: FC<TStoreWrapper> = observer((props) => {
|
||||
* Setting up the theme of the user by fetching it from local storage
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!userProfile) return;
|
||||
if (window) setDom(window.document?.querySelector<HTMLElement>("[data-theme='custom']") || undefined);
|
||||
if (!userProfile?.theme?.theme) return;
|
||||
if (window) setDom(() => window.document?.querySelector<HTMLElement>("[data-theme='custom']") || undefined);
|
||||
|
||||
setTheme(userProfile?.theme?.theme || "system");
|
||||
if (userProfile?.theme?.theme === "custom" && userProfile?.theme?.palette && dom)
|
||||
applyTheme(userProfile?.theme?.palette, false);
|
||||
applyTheme(
|
||||
userProfile?.theme?.palette !== ",,,,"
|
||||
? userProfile?.theme?.palette
|
||||
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
|
||||
false
|
||||
);
|
||||
else unsetCustomCssVariables();
|
||||
}, [userProfile, setTheme, dom]);
|
||||
}, [userProfile, userProfile?.theme?.theme, userProfile?.theme?.palette, setTheme, dom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.query) return;
|
||||
|
@ -2,64 +2,55 @@ import { useEffect, useState, ReactElement } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
// ui
|
||||
import {
|
||||
Spinner,
|
||||
// setPromiseToast
|
||||
} from "@plane/ui";
|
||||
import { Spinner, setPromiseToast } from "@plane/ui";
|
||||
// components
|
||||
import { CustomThemeSelector, ThemeSwitch, PageHead } from "@/components/core";
|
||||
// constants
|
||||
import { I_THEME_OPTION, THEME_OPTIONS } from "@/constants/themes";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useUserProfile } from "@/hooks/store";
|
||||
// layouts
|
||||
import { ProfilePreferenceSettingsLayout } from "@/layouts/settings-layout/profile/preferences";
|
||||
// type
|
||||
import { NextPageWithLayout } from "@/lib/types";
|
||||
|
||||
const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => {
|
||||
const { setTheme } = useTheme();
|
||||
// states
|
||||
const [currentTheme, setCurrentTheme] = useState<I_THEME_OPTION | null>(null);
|
||||
// store hooks
|
||||
const {
|
||||
data: currentUser,
|
||||
userProfile: { data: userProfile },
|
||||
} = useUser();
|
||||
// computed
|
||||
const userTheme = userProfile?.theme;
|
||||
// hooks
|
||||
const { setTheme } = useTheme();
|
||||
const { data: userProfile, updateUserTheme } = useUserProfile();
|
||||
|
||||
useEffect(() => {
|
||||
if (userTheme) {
|
||||
const userThemeOption = THEME_OPTIONS.find((t) => t.value === userTheme?.theme);
|
||||
if (userProfile?.theme?.theme) {
|
||||
const userThemeOption = THEME_OPTIONS.find((t) => t.value === userProfile?.theme?.theme);
|
||||
if (userThemeOption) {
|
||||
setCurrentTheme(userThemeOption);
|
||||
}
|
||||
}
|
||||
}, [userTheme]);
|
||||
}, [userProfile?.theme?.theme]);
|
||||
|
||||
const handleThemeChange = (themeOption: I_THEME_OPTION) => {
|
||||
setTheme(themeOption.value);
|
||||
// const updateCurrentUserThemePromise = updateCurrentUserTheme(themeOption.value);
|
||||
const updateCurrentUserThemePromise = updateUserTheme({ theme: themeOption.value });
|
||||
|
||||
// setPromiseToast(updateCurrentUserThemePromise, {
|
||||
// loading: "Updating theme...",
|
||||
// success: {
|
||||
// title: "Success!",
|
||||
// message: () => "Theme updated successfully!",
|
||||
// },
|
||||
// error: {
|
||||
// title: "Error!",
|
||||
// message: () => "Failed to Update the theme",
|
||||
// },
|
||||
// });
|
||||
setPromiseToast(updateCurrentUserThemePromise, {
|
||||
loading: "Updating theme...",
|
||||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Theme updated successfully!",
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
message: () => "Failed to Update the theme",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title="Profile - Theme Prefrence" />
|
||||
{currentUser ? (
|
||||
{userProfile ? (
|
||||
<div className="mx-auto mt-10 h-full w-full overflow-y-auto md:px-6 px-4 pb-8 md:mt-14 lg:px-20 vertical-scrollbar scrollbar-md">
|
||||
<div className="flex items-center border-b border-custom-border-100 pb-3.5">
|
||||
<h3 className="text-xl font-medium">Preferences</h3>
|
||||
@ -73,7 +64,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => {
|
||||
<ThemeSwitch value={currentTheme} onChange={handleThemeChange} />
|
||||
</div>
|
||||
</div>
|
||||
{userTheme?.theme === "custom" && <CustomThemeSelector />}
|
||||
{userProfile?.theme?.theme === "custom" && <CustomThemeSelector />}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
|
||||
|
@ -25,13 +25,15 @@ export class AuthService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
signUpEmailCheck = async (data: IEmailCheckData): Promise<IEmailCheckResponse> => this.post("/auth/sign-up/email-check/", data, { headers: {} })
|
||||
signUpEmailCheck = async (data: IEmailCheckData): Promise<IEmailCheckResponse> =>
|
||||
this.post("/auth/sign-up/email-check/", data, { headers: {} })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
||||
signInEmailCheck = async (data: IEmailCheckData): Promise<IEmailCheckResponse> => this.post("/auth/sign-in/email-check/", data, { headers: {} })
|
||||
signInEmailCheck = async (data: IEmailCheckData): Promise<IEmailCheckResponse> =>
|
||||
this.post("/auth/sign-in/email-check/", data, { headers: {} })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
@ -77,6 +77,9 @@ export class RootStore {
|
||||
}
|
||||
|
||||
resetOnSignOut() {
|
||||
// handling the system theme when user logged out from the app
|
||||
localStorage.setItem("theme", "system");
|
||||
|
||||
this.workspaceRoot = new WorkspaceRootStore(this);
|
||||
this.projectRoot = new ProjectRootStore(this);
|
||||
this.memberRoot = new MemberRootStore(this);
|
||||
|
@ -1,18 +1,15 @@
|
||||
// mobx
|
||||
import { action, observable, makeObservable } from "mobx";
|
||||
// helper
|
||||
import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper";
|
||||
// store types
|
||||
import { RootStore } from "@/store/root.store";
|
||||
|
||||
export interface IThemeStore {
|
||||
// observables
|
||||
theme: string | null;
|
||||
sidebarCollapsed: boolean | undefined;
|
||||
profileSidebarCollapsed: boolean | undefined;
|
||||
workspaceAnalyticsSidebarCollapsed: boolean | undefined;
|
||||
issueDetailSidebarCollapsed: boolean | undefined;
|
||||
// actions
|
||||
toggleSidebar: (collapsed?: boolean) => void;
|
||||
setTheme: (theme: any) => void;
|
||||
toggleProfileSidebar: (collapsed?: boolean) => void;
|
||||
toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void;
|
||||
toggleIssueDetailSidebar: (collapsed?: boolean) => void;
|
||||
@ -21,31 +18,23 @@ export interface IThemeStore {
|
||||
export class ThemeStore implements IThemeStore {
|
||||
// observables
|
||||
sidebarCollapsed: boolean | undefined = undefined;
|
||||
theme: string | null = null;
|
||||
profileSidebarCollapsed: boolean | undefined = undefined;
|
||||
workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined;
|
||||
issueDetailSidebarCollapsed: boolean | undefined = undefined;
|
||||
// root store
|
||||
rootStore;
|
||||
|
||||
constructor(_rootStore: any | null = null) {
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
sidebarCollapsed: observable.ref,
|
||||
theme: observable.ref,
|
||||
profileSidebarCollapsed: observable.ref,
|
||||
workspaceAnalyticsSidebarCollapsed: observable.ref,
|
||||
issueDetailSidebarCollapsed: observable.ref,
|
||||
// action
|
||||
toggleSidebar: action,
|
||||
setTheme: action,
|
||||
toggleProfileSidebar: action,
|
||||
toggleWorkspaceAnalyticsSidebar: action,
|
||||
toggleIssueDetailSidebar: action,
|
||||
// computed
|
||||
});
|
||||
// root store
|
||||
this.rootStore = _rootStore;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,30 +84,4 @@ export class ThemeStore implements IThemeStore {
|
||||
}
|
||||
localStorage.setItem("issue_detail_sidebar_collapsed", this.issueDetailSidebarCollapsed.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the user theme and applies it to the platform
|
||||
* @param _theme
|
||||
*/
|
||||
setTheme = async (_theme: { theme: any }) => {
|
||||
try {
|
||||
const currentTheme: string = _theme?.theme?.theme?.toString();
|
||||
// updating the local storage theme value
|
||||
localStorage.setItem("theme", currentTheme);
|
||||
// updating the mobx theme value
|
||||
this.theme = currentTheme;
|
||||
// applying the theme to platform if the selected theme is custom
|
||||
if (currentTheme === "custom") {
|
||||
const themeSettings = this.rootStore.user.currentUserSettings || null;
|
||||
applyTheme(
|
||||
themeSettings?.theme?.palette !== ",,,,"
|
||||
? themeSettings?.theme?.palette
|
||||
: "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
|
||||
themeSettings?.theme?.darkPalette
|
||||
);
|
||||
} else unsetCustomCssVariables();
|
||||
} catch (error) {
|
||||
console.error("setting user theme error", error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export class UserStore implements IUserStore {
|
||||
|
||||
constructor(private store: RootStore) {
|
||||
// stores
|
||||
this.userProfile = new ProfileStore();
|
||||
this.userProfile = new ProfileStore(store);
|
||||
this.userSettings = new UserSettingsStore();
|
||||
this.membership = new UserMembershipStore(store);
|
||||
// service
|
||||
|
@ -1,9 +1,11 @@
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import set from "lodash/set";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
// services
|
||||
import { UserService } from "services/user.service";
|
||||
// types
|
||||
import { TUserProfile } from "@plane/types";
|
||||
import { IUserTheme, TUserProfile } from "@plane/types";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
|
||||
type TError = {
|
||||
status: string;
|
||||
@ -20,6 +22,7 @@ export interface IUserProfileStore {
|
||||
updateUserProfile: (data: Partial<TUserProfile>) => Promise<TUserProfile | undefined>;
|
||||
updateUserOnBoard: () => Promise<TUserProfile | undefined>;
|
||||
updateTourCompleted: () => Promise<TUserProfile | undefined>;
|
||||
updateUserTheme: (data: Partial<IUserTheme>) => Promise<TUserProfile | undefined>;
|
||||
}
|
||||
|
||||
export class ProfileStore implements IUserProfileStore {
|
||||
@ -59,7 +62,7 @@ export class ProfileStore implements IUserProfileStore {
|
||||
// services
|
||||
userService: UserService;
|
||||
|
||||
constructor() {
|
||||
constructor(public store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isLoading: observable.ref,
|
||||
@ -70,6 +73,7 @@ export class ProfileStore implements IUserProfileStore {
|
||||
updateUserProfile: action,
|
||||
updateUserOnBoard: action,
|
||||
updateTourCompleted: action,
|
||||
updateUserTheme: action,
|
||||
});
|
||||
// services
|
||||
this.userService = new UserService();
|
||||
@ -179,4 +183,34 @@ export class ProfileStore implements IUserProfileStore {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description updates the user theme
|
||||
* @returns @returns {Promise<TUserProfile | undefined>}
|
||||
*/
|
||||
updateUserTheme = async (data: Partial<IUserTheme>) => {
|
||||
const currentProfileTheme = cloneDeep(this.data.theme);
|
||||
try {
|
||||
runInAction(() => {
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
const userKey: keyof IUserTheme = key as keyof IUserTheme;
|
||||
if (this.data.theme) set(this.data.theme, userKey, data[userKey]);
|
||||
});
|
||||
});
|
||||
const userProfile = await this.userService.updateCurrentUserProfile({ theme: this.data.theme });
|
||||
return userProfile;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
const userKey: keyof IUserTheme = key as keyof IUserTheme;
|
||||
if (currentProfileTheme) set(this.data.theme, userKey, currentProfileTheme[userKey]);
|
||||
});
|
||||
this.error = {
|
||||
status: "user-profile-theme-update-error",
|
||||
message: "Failed to update user profile theme",
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1370,7 +1370,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
|
||||
integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
|
||||
|
||||
"@headlessui/react@^1.7.13", "@headlessui/react@^1.7.17", "@headlessui/react@^1.7.3":
|
||||
"@headlessui/react@^1.7.13", "@headlessui/react@^1.7.17", "@headlessui/react@^1.7.19", "@headlessui/react@^1.7.3":
|
||||
version "1.7.19"
|
||||
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.19.tgz#91c78cf5fcb254f4a0ebe96936d48421caf75f40"
|
||||
integrity sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==
|
||||
|
Loading…
Reference in New Issue
Block a user