From 1fd150c189f5da0fa40860d63d4d3e4ad6213f43 Mon Sep 17 00:00:00 2001 From: gurusainath Date: Fri, 3 May 2024 17:52:33 +0530 Subject: [PATCH 1/6] chore: updated cookie session time for admin --- apiserver/plane/settings/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 5c98d345c..e0eda6d8a 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -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 From 95679bc27f7bf964935b0236a76573893409d975 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 6 May 2024 02:17:24 +0530 Subject: [PATCH 2/6] chore: add email security dropdown and remove SMTP username and password validation. --- admin/app/ai/components/ai-config-form.tsx | 2 +- .../email/components/email-config-form.tsx | 145 +++++++++++++----- admin/components/common/controller-input.tsx | 8 +- admin/package.json | 1 + packages/ui/src/dropdowns/custom-select.tsx | 19 ++- yarn.lock | 4 +- 6 files changed, 125 insertions(+), 54 deletions(-) diff --git a/admin/app/ai/components/ai-config-form.tsx b/admin/app/ai/components/ai-config-form.tsx index 5290ed1e2..d61eb9ed9 100644 --- a/admin/app/ai/components/ai-config-form.tsx +++ b/admin/app/ai/components/ai-config-form.tsx @@ -96,7 +96,7 @@ export const InstanceAIForm: FC = (props) => {
OpenAI
If you use ChatGPT, this is for you.
-
+
{aiFormFields.map((field) => ( ; +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 = (props) => { const { config } = props; // states @@ -26,8 +34,9 @@ export const InstanceEmailForm: FC = (props) => { const { handleSubmit, watch, + setValue, control, - formState: { errors, isSubmitting }, + formState: { errors, isValid, isDirty, isSubmitting }, } = useForm({ defaultValues: { EMAIL_HOST: config["EMAIL_HOST"], @@ -35,7 +44,7 @@ export const InstanceEmailForm: FC = (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 = (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 = (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 = (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 (
setIsSendTestEmailModalOpen(false)} /> -
+
{emailFormFields.map((field) => ( = (props) => { required={field.required} /> ))} +
+

Email security

+ + {Object.entries(EMAIL_SECURITY_OPTIONS).map(([key, value]) => ( + + {value} + + ))} + +
-
-
-
-
- Turn TLS {Boolean(parseInt(watch("EMAIL_USE_TLS"))) ? "off" : "on"} -
-
- Use this if your email domain supports TLS. +
+
+
+
+
Authentication (optional)
+
+ We recommend setting up a username password for your SMTP server +
-
- +
+ {OptionalEmailFormFields.map((field) => ( + ( - { - 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} /> -
+ ))}
-
- -
diff --git a/admin/components/common/controller-input.tsx b/admin/components/common/controller-input.tsx index c386e7374..d47fe43f9 100644 --- a/admin/components/common/controller-input.tsx +++ b/admin/components/common/controller-input.tsx @@ -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; @@ -51,7 +53,9 @@ export const ControllerInput: React.FC = (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) => { ))}
- {description &&

{description}

} + {description &&

{description}

}
); }; diff --git a/admin/package.json b/admin/package.json index e0913d094..956d39c1a 100644 --- a/admin/package.json +++ b/admin/package.json @@ -11,6 +11,7 @@ "lint": "next lint" }, "dependencies": { + "@headlessui/react": "^1.7.19", "@plane/types": "*", "@plane/ui": "*", "@tailwindcss/typography": "^0.5.9", diff --git a/packages/ui/src/dropdowns/custom-select.tsx b/packages/ui/src/dropdowns/custom-select.tsx index 2d669cc05..17933da42 100644 --- a/packages/ui/src/dropdowns/custom-select.tsx +++ b/packages/ui/src/dropdowns/custom-select.tsx @@ -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} @@ -77,12 +77,17 @@ const CustomSelect = (props: ICustomSelectProps) => { - - + + + + {!isSidebarCollapsed && "Redirect to plane"} + + + + + + + +
From b9422bc94a4fd5f3ab4a4cd54751507efcac45ea Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 6 May 2024 02:27:08 +0530 Subject: [PATCH 4/6] chore: add dropdown to collapsed admin sidebar. --- .../admin-sidebar/sidebar-dropdown.tsx | 101 +++++++++++------- 1 file changed, 65 insertions(+), 36 deletions(-) diff --git a/admin/components/admin-sidebar/sidebar-dropdown.tsx b/admin/components/admin-sidebar/sidebar-dropdown.tsx index e2328d576..68212464e 100644 --- a/admin/components/admin-sidebar/sidebar-dropdown.tsx +++ b/admin/components/admin-sidebar/sidebar-dropdown.tsx @@ -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 = () => ( + +
+ {currentUser?.email} +
+
+ + + Switch to {resolvedTheme === "dark" ? "light" : "dark"} mode + +
+
+
+ + + + Sign out + +
+
+
+ ); + useEffect(() => { if (csrfToken === undefined) authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token)); @@ -45,9 +84,30 @@ export const SidebarDropdown = observer(() => { isSidebarCollapsed ? "justify-center" : "" }`} > -
- -
+ + +
+ +
+
+ {isSidebarCollapsed && ( + + {getSidebarMenuItems()} + + )} +
{!isSidebarCollapsed && (
@@ -78,38 +138,7 @@ export const SidebarDropdown = observer(() => { leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - -
- {currentUser?.email} -
-
- - - Switch to {resolvedTheme === "dark" ? "light" : "dark"} mode - -
-
-
- - - - Sign out - -
-
-
+ {getSidebarMenuItems()} )} From e95cd1e38530678e19ca08fc1be8770529f2f024 Mon Sep 17 00:00:00 2001 From: gurusainath Date: Mon, 6 May 2024 14:00:19 +0530 Subject: [PATCH 5/6] chore: profile themning --- packages/types/src/users.d.ts | 20 ++++---- .../core/theme/custom-theme-selector.tsx | 45 +++++++++------- web/lib/wrappers/store-wrapper.tsx | 19 ++++--- web/pages/profile/preferences/theme.tsx | 51 ++++++++----------- web/store/theme.store.ts | 43 ++-------------- web/store/user/profile.store.ts | 38 +++++++++++++- yarn.lock | 2 +- 7 files changed, 108 insertions(+), 110 deletions(-) diff --git a/packages/types/src/users.d.ts b/packages/types/src/users.d.ts index 452455876..c191cac89 100644 --- a/packages/types/src/users.d.ts +++ b/packages/types/src/users.d.ts @@ -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 { diff --git a/web/components/core/theme/custom-theme-selector.tsx b/web/components/core/theme/custom-theme-selector.tsx index 9309a3bfe..d7e115a19 100644 --- a/web/components/core/theme/custom-theme-selector.tsx +++ b/web/components/core/theme/custom-theme-selector.tsx @@ -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({ 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) => { 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) => { diff --git a/web/lib/wrappers/store-wrapper.tsx b/web/lib/wrappers/store-wrapper.tsx index 1d9a4f9c1..ec5b79bfa 100644 --- a/web/lib/wrappers/store-wrapper.tsx +++ b/web/lib/wrappers/store-wrapper.tsx @@ -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 = 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(); @@ -40,14 +38,19 @@ const StoreWrapper: FC = 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("[data-theme='custom']") || undefined); + if (!userProfile?.theme?.theme) return; + if (window) setDom(() => window.document?.querySelector("[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; diff --git a/web/pages/profile/preferences/theme.tsx b/web/pages/profile/preferences/theme.tsx index a80001eda..7c5593333 100644 --- a/web/pages/profile/preferences/theme.tsx +++ b/web/pages/profile/preferences/theme.tsx @@ -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(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 ( <> - {currentUser ? ( + {userProfile ? (

Preferences

@@ -73,7 +64,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => {
- {userTheme?.theme === "custom" && } + {userProfile?.theme?.theme === "custom" && }
) : (
diff --git a/web/store/theme.store.ts b/web/store/theme.store.ts index da499a63c..33537fe79 100644 --- a/web/store/theme.store.ts +++ b/web/store/theme.store.ts @@ -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); - } - }; } diff --git a/web/store/user/profile.store.ts b/web/store/user/profile.store.ts index 51c9f0a2d..606cbe6bf 100644 --- a/web/store/user/profile.store.ts +++ b/web/store/user/profile.store.ts @@ -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) => Promise; updateUserOnBoard: () => Promise; updateTourCompleted: () => Promise; + updateUserTheme: (data: Partial) => Promise; } 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} + */ + updateUserTheme = async (data: Partial) => { + 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; + } + }; } diff --git a/yarn.lock b/yarn.lock index f8ed88233..50d914594 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2759,7 +2759,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^18.2.42", "@types/react@^18.2.48": +"@types/react@*", "@types/react@18.2.48", "@types/react@^18.2.42", "@types/react@^18.2.48": version "18.2.48" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1" integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w== From e4e4076c3a4ca6b14d87f0021f701fed35dc7460 Mon Sep 17 00:00:00 2001 From: gurusainath Date: Mon, 6 May 2024 18:19:24 +0530 Subject: [PATCH 6/6] chore: updated page error messages and theme in command palette --- .../plane/authentication/views/app/email.py | 1 + .../account/auth-forms/sign-in-root.tsx | 10 +- .../account/auth-forms/sign-up-root.tsx | 10 +- .../command-palette/actions/theme-actions.tsx | 26 +++-- web/helpers/authentication.helper.ts | 105 ++++++++++++------ .../settings-layout/profile/sidebar.tsx | 13 +-- web/services/auth.service.ts | 6 +- web/store/root.store.ts | 3 + web/store/user/index.ts | 2 +- 9 files changed, 114 insertions(+), 62 deletions(-) diff --git a/apiserver/plane/authentication/views/app/email.py b/apiserver/plane/authentication/views/app/email.py index 50c3e2eb2..d419b85b4 100644 --- a/apiserver/plane/authentication/views/app/email.py +++ b/apiserver/plane/authentication/views/app/email.py @@ -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), } diff --git a/web/components/account/auth-forms/sign-in-root.tsx b/web/components/account/auth-forms/sign-in-root.tsx index 9115a335c..78a65a63d 100644 --- a/web/components/account/auth-forms/sign-in-root.tsx +++ b/web/components/account/auth-forms/sign-in-root.tsx @@ -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]); diff --git a/web/components/account/auth-forms/sign-up-root.tsx b/web/components/account/auth-forms/sign-up-root.tsx index fc3af3bd1..0ea698674 100644 --- a/web/components/account/auth-forms/sign-up-root.tsx +++ b/web/components/account/auth-forms/sign-up-root.tsx @@ -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]); diff --git a/web/components/command-palette/actions/theme-actions.tsx b/web/components/command-palette/actions/theme-actions.tsx index 9d470ba85..7b0ddc72b 100644 --- a/web/components/command-palette/actions/theme-actions.tsx +++ b/web/components/command-palette/actions/theme-actions.tsx @@ -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 = 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 = observer((props) => { { - updateUserTheme(theme.value); + updateTheme(theme.value); closePalette(); }} className="focus:outline-none" diff --git a/web/helpers/authentication.helper.ts b/web/helpers/authentication.helper.ts index ca0bfdfe5..375d2a9fa 100644 --- a/web/helpers/authentication.helper.ts +++ b/web/helpers/authentication.helper.ts @@ -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; diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx index 1e6ffc35b..9e7fc95cc 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -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(null); diff --git a/web/services/auth.service.ts b/web/services/auth.service.ts index d50b9fcff..c6ee23c79 100644 --- a/web/services/auth.service.ts +++ b/web/services/auth.service.ts @@ -25,13 +25,15 @@ export class AuthService extends APIService { }); } - signUpEmailCheck = async (data: IEmailCheckData): Promise => this.post("/auth/sign-up/email-check/", data, { headers: {} }) + signUpEmailCheck = async (data: IEmailCheckData): Promise => + this.post("/auth/sign-up/email-check/", data, { headers: {} }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; }); - signInEmailCheck = async (data: IEmailCheckData): Promise => this.post("/auth/sign-in/email-check/", data, { headers: {} }) + signInEmailCheck = async (data: IEmailCheckData): Promise => + this.post("/auth/sign-in/email-check/", data, { headers: {} }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/store/root.store.ts b/web/store/root.store.ts index 95e25d2b4..b76fc4756 100644 --- a/web/store/root.store.ts +++ b/web/store/root.store.ts @@ -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); diff --git a/web/store/user/index.ts b/web/store/user/index.ts index 6bc7c23f3..d38044b1d 100644 --- a/web/store/user/index.ts +++ b/web/store/user/index.ts @@ -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