From 98b1a078de253e7df826bf887763d01fb163087c Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Tue, 17 Oct 2023 12:46:38 +0530 Subject: [PATCH] fix: project layout added and theming fixes (#2455) * fix: project layout added and theme fixes * feat: input color picker component added to ui package * fix: layout fixes * fix: conflicts and build issues resolved * fix: layout headers fixes --- packages/ui/package.json | 5 +- packages/ui/src/form-fields/index.tsx | 1 + .../ui/src/form-fields/input-color-picker.tsx | 91 +++++++++ .../change-interface-theme.tsx | 9 +- .../core/theme/color-picker-input.tsx | 8 +- .../core/theme/custom-theme-selector.tsx | 159 ++++++++------- web/components/core/theme/theme-switch.tsx | 180 ++++++----------- web/components/headers/cycles.tsx | 56 ++++++ web/components/headers/index.ts | 3 + web/components/headers/modules.tsx | 87 ++++++++ .../headers/profile-preferences.tsx | 31 +++ web/components/headers/project-issues.tsx | 148 ++++++++------ web/components/headers/project-views.tsx | 70 +++++-- web/components/icons/module-icon.tsx | 17 +- web/constants/themes.ts | 13 +- web/layouts/app-layout/layout.tsx | 10 +- web/layouts/auth-layout/index.ts | 1 + web/layouts/auth-layout/project-wrapper.tsx | 91 +++++++++ web/layouts/auth-layout/user-wrapper.tsx | 13 +- web/layouts/auth-layout/workspace-wrapper.tsx | 11 +- web/lib/mobx/store-init.tsx | 36 ++-- .../me/profile/preferences.tsx | 132 ++++++------ .../projects/[projectId]/cycles/index.tsx | 34 +--- .../projects/[projectId]/inbox/[inboxId].tsx | 6 +- .../projects/[projectId]/issues/index.tsx | 73 +------ .../projects/[projectId]/modules/index.tsx | 77 ++----- .../projects/[projectId]/views/index.tsx | 27 +-- web/store/project/project.store.ts | 9 +- web/store/user.store.ts | 188 ++++++++---------- web/types/users.d.ts | 4 +- 30 files changed, 891 insertions(+), 699 deletions(-) create mode 100644 packages/ui/src/form-fields/input-color-picker.tsx create mode 100644 web/components/headers/cycles.tsx create mode 100644 web/components/headers/modules.tsx create mode 100644 web/components/headers/profile-preferences.tsx create mode 100644 web/layouts/auth-layout/project-wrapper.tsx diff --git a/packages/ui/package.json b/packages/ui/package.json index 4eb7ac8b8..2e906cb30 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -22,8 +22,8 @@ "classnames": "^2.3.2", "eslint-config-custom": "*", "react": "^18.2.0", - "tsconfig": "*", "tailwind-config-custom": "*", + "tsconfig": "*", "tsup": "^5.10.1", "typescript": "4.7.4" }, @@ -33,6 +33,7 @@ "dependencies": { "@blueprintjs/core": "^4.16.3", "@blueprintjs/popover2": "^1.13.3", - "@headlessui/react": "^1.7.17" + "@headlessui/react": "^1.7.17", + "react-color": "^2.19.3" } } diff --git a/packages/ui/src/form-fields/index.tsx b/packages/ui/src/form-fields/index.tsx index 01cd4edea..49f6f1552 100644 --- a/packages/ui/src/form-fields/index.tsx +++ b/packages/ui/src/form-fields/index.tsx @@ -1,2 +1,3 @@ export * from "./input"; export * from "./textarea"; +export * from "./input-color-picker" \ No newline at end of file diff --git a/packages/ui/src/form-fields/input-color-picker.tsx b/packages/ui/src/form-fields/input-color-picker.tsx new file mode 100644 index 000000000..1fdcf8c5e --- /dev/null +++ b/packages/ui/src/form-fields/input-color-picker.tsx @@ -0,0 +1,91 @@ +import * as React from "react"; +import { Popover, Transition } from "@headlessui/react"; +import { ColorResult, SketchPicker } from "react-color"; +// components +import { Input } from "./input"; + +export interface InputColorPickerProps { + hasError: boolean; + value: string | undefined; + onChange: (value: string) => void; + name: string; + className: string; + placeholder: string; +} + +export const InputColorPicker: React.FC = (props) => { + const { value, hasError, onChange, name, className, placeholder } = props; + + const handleColorChange = (newColor: ColorResult) => { + const { hex } = newColor; + onChange(hex); + }; + + const handleInputChange = (value: any) => { + onChange(value); + }; + + return ( +
+ +
+ + {({ open }) => ( + <> + + {value && value !== "" ? ( + + ) : ( + + + + )} + + + + + + + + + )} + +
+
+ ); +}; diff --git a/web/components/command-palette/change-interface-theme.tsx b/web/components/command-palette/change-interface-theme.tsx index ee5d12d29..5e5651d93 100644 --- a/web/components/command-palette/change-interface-theme.tsx +++ b/web/components/command-palette/change-interface-theme.tsx @@ -1,14 +1,9 @@ import React, { Dispatch, SetStateAction, useEffect, useState } from "react"; - -// cmdk import { Command } from "cmdk"; -import { THEMES_OBJ } from "constants/themes"; +import { THEME_OPTIONS } from "constants/themes"; import { useTheme } from "next-themes"; import useUser from "hooks/use-user"; import { Settings } from "lucide-react"; -// helper -import { unsetCustomCssVariables } from "helpers/theme.helper"; -// mobx react lite import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; @@ -44,7 +39,7 @@ export const ChangeInterfaceTheme: React.FC = observer(({ setIsPaletteOpe return ( <> - {THEMES_OBJ.filter((t) => t.value !== "custom").map((theme) => ( + {THEME_OPTIONS.filter((t) => t.value !== "custom").map((theme) => ( { diff --git a/web/components/core/theme/color-picker-input.tsx b/web/components/core/theme/color-picker-input.tsx index 331008c47..f47c1349f 100644 --- a/web/components/core/theme/color-picker-input.tsx +++ b/web/components/core/theme/color-picker-input.tsx @@ -18,14 +18,14 @@ import { Input } from "@plane/ui"; // icons import { Palette } from "lucide-react"; // types -import { ICustomTheme } from "types"; +import { IUserTheme } from "types"; type Props = { - name: keyof ICustomTheme; + name: keyof IUserTheme; position?: "left" | "right"; watch: UseFormWatch; setValue: UseFormSetValue; - control: Control; + control: Control; error: FieldError | Merge> | undefined; register: UseFormRegister; }; @@ -38,7 +38,7 @@ export const ColorPickerInput: FC = (props) => { setValue(name, hex); }; - const getColorText = (colorName: keyof ICustomTheme) => { + const getColorText = (colorName: keyof IUserTheme) => { switch (colorName) { case "background": return "Background"; diff --git a/web/components/core/theme/custom-theme-selector.tsx b/web/components/core/theme/custom-theme-selector.tsx index f70f9e80a..dea5557d5 100644 --- a/web/components/core/theme/custom-theme-selector.tsx +++ b/web/components/core/theme/custom-theme-selector.tsx @@ -1,77 +1,54 @@ -import React, { useEffect, useState } from "react"; - +import { FC } from "react"; import { useTheme } from "next-themes"; - -import { useForm } from "react-hook-form"; - +import { Controller, useForm } from "react-hook-form"; // ui -import { ColorPickerInput } from "components/core"; -import { Button } from "@plane/ui"; +import { Button, InputColorPicker } from "@plane/ui"; // types -import { ICustomTheme } from "types"; +import { IUserTheme } from "types"; // mobx react lite import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -type Props = { - preLoadedData?: Partial | null; -}; +type Props = {}; -const defaultValues: ICustomTheme = { - background: "#0d101b", - text: "#c5c5c5", - primary: "#3f76ff", - sidebarBackground: "#0d101b", - sidebarText: "#c5c5c5", - darkPalette: false, - palette: "", - theme: "custom", -}; - -export const CustomThemeSelector: React.FC = observer(({ preLoadedData }) => { - const store: any = useMobxStore(); +export const CustomThemeSelector: FC = observer(() => { + const { user: userStore } = useMobxStore(); + const userTheme = userStore?.currentUser?.theme; + // hooks const { setTheme } = useTheme(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [darkPalette, setDarkPalette] = useState(false); - const { - register, formState: { errors, isSubmitting }, handleSubmit, control, - watch, - setValue, - reset, - } = useForm({ - defaultValues, + } = 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 : "", + }, }); - useEffect(() => { - reset({ - ...defaultValues, - ...preLoadedData, - }); - }, [preLoadedData, reset]); const handleUpdateTheme = async (formData: any) => { - const payload: ICustomTheme = { + const payload: IUserTheme = { background: formData.background, text: formData.text, primary: formData.primary, sidebarBackground: formData.sidebarBackground, sidebarText: formData.sidebarText, - darkPalette: darkPalette, + darkPalette: false, palette: `${formData.background},${formData.text},${formData.primary},${formData.sidebarBackground},${formData.sidebarText}`, theme: "custom", }; setTheme("custom"); - return store.user - .updateCurrentUserSettings({ theme: payload }) - .then((response: any) => response) - .catch((error: any) => error); + return userStore.updateCurrentUser({ theme: payload }); }; return ( @@ -82,63 +59,91 @@ export const CustomThemeSelector: React.FC = observer(({ preLoadedData })

Background color

- ( + + )} />

Text color

- ( + + )} />

Primary(Theme) color

- ( + + )} />

Sidebar background color

- ( + + )} />

Sidebar text color

- ( + + )} />
diff --git a/web/components/core/theme/theme-switch.tsx b/web/components/core/theme/theme-switch.tsx index 2c169ec44..bb3133c60 100644 --- a/web/components/core/theme/theme-switch.tsx +++ b/web/components/core/theme/theme-switch.tsx @@ -1,130 +1,80 @@ -// next-themes -import { useTheme } from "next-themes"; -// hooks -import useUser from "hooks/use-user"; +import { FC } from "react"; // constants -import { THEMES_OBJ } from "constants/themes"; +import { THEME_OPTIONS, I_THEME_OPTION } from "constants/themes"; // ui import { CustomSelect } from "components/ui"; -// types -import { ICustomTheme } from "types"; -import { unsetCustomCssVariables } from "helpers/theme.helper"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; type Props = { - setPreLoadedData: React.Dispatch>; - customThemeSelectorOptions: boolean; - setCustomThemeSelectorOptions: React.Dispatch>; + value: I_THEME_OPTION | null; + onChange: (value: I_THEME_OPTION) => void; }; -export const ThemeSwitch: React.FC = observer( - ({ setPreLoadedData, customThemeSelectorOptions, setCustomThemeSelectorOptions }) => { - const store: any = useMobxStore(); +export const ThemeSwitch: FC = (props) => { + const { value, onChange } = props; - const { user } = useUser(); - const { theme, setTheme } = useTheme(); - - const updateUserTheme = (newTheme: string) => { - if (!user) return; - setTheme(newTheme); - return store.user - .updateCurrentUserSettings({ theme: { ...user.theme, theme: newTheme } }) - .then((response: any) => response) - .catch((error: any) => error); - }; - - const currentThemeObj = THEMES_OBJ.find((t) => t.value === theme); - - return ( - + return ( + +
-
-
-
- {currentThemeObj.label} -
- ) : ( - "Select your theme" - ) - } - onChange={({ value, type }: { value: string; type: string }) => { - if (value === "custom") { - if (user?.theme?.palette) { - setPreLoadedData({ - background: user.theme?.background !== "" ? user.theme.background : "#0d101b", - text: user.theme.text !== "" ? user.theme.text : "#c5c5c5", - primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff", - sidebarBackground: user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b", - sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5", - darkPalette: false, - palette: user.theme.palette !== ",,,," ? user.theme.palette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5", - theme: "custom", - }); - } - - if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true); - } else { - if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false); - unsetCustomCssVariables(); - } - - updateUserTheme(value); - document.documentElement.style.setProperty("--color-scheme", type); - }} - input - width="w-full" - > - {THEMES_OBJ.map(({ value, label, type, icon }) => ( - -
+ />
-
-
-
- {label} + />
- - ))} - - ); - } -); + {value.label} +
+ ) : ( + "Select your theme" + ) + } + onChange={onChange} + input + width="w-full" + > + {THEME_OPTIONS.map((themeOption) => ( + +
+
+
+
+
+ {themeOption.label} +
+ + ))} + + ); +}; diff --git a/web/components/headers/cycles.tsx b/web/components/headers/cycles.tsx new file mode 100644 index 000000000..40238aa8f --- /dev/null +++ b/web/components/headers/cycles.tsx @@ -0,0 +1,56 @@ +import { FC } from "react"; +import { useRouter } from "next/router"; +import { ArrowLeft, Plus } from "lucide-react"; +// components +import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs"; +// ui +import { Button } from "@plane/ui"; +// helpers +import { truncateText } from "helpers/string.helper"; + +export interface ICyclesHeader { + name: string | undefined; +} + +export const CyclesHeader: FC = (props) => { + const { name } = props; + // router + const router = useRouter(); + const { workspaceSlug } = router.query; + + return ( +
+
+
+ +
+
+ + + + +
+
+
+ +
+
+ ); +}; diff --git a/web/components/headers/index.ts b/web/components/headers/index.ts index 2bd01abe2..abb782f09 100644 --- a/web/components/headers/index.ts +++ b/web/components/headers/index.ts @@ -8,3 +8,6 @@ export * from "./project-views"; export * from "./workspace-analytics"; export * from "./workspace-dashboard"; export * from "./projects"; +export * from "./profile-preferences"; +export * from "./cycles"; +export * from "./modules"; diff --git a/web/components/headers/modules.tsx b/web/components/headers/modules.tsx new file mode 100644 index 000000000..ef9fc670a --- /dev/null +++ b/web/components/headers/modules.tsx @@ -0,0 +1,87 @@ +import { Dispatch, FC, SetStateAction } from "react"; +import { useRouter } from "next/router"; +import { ArrowLeft, Plus } from "lucide-react"; +// components +import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs"; +// ui +import { Button, Tooltip } from "@plane/ui"; +import { Icon } from "components/ui"; +// helper +import { replaceUnderscoreIfSnakeCase, truncateText } from "helpers/string.helper"; + +export interface IModulesHeader { + name: string | undefined; + modulesView: string; + setModulesView: Dispatch>; +} + +const moduleViewOptions: { type: "grid" | "gantt_chart"; icon: any }[] = [ + { + type: "gantt_chart", + icon: "view_timeline", + }, + { + type: "grid", + icon: "table_rows", + }, +]; + +export const ModulesHeader: FC = (props) => { + const { name, modulesView, setModulesView } = props; + // router + const router = useRouter(); + const { workspaceSlug } = router.query; + + return ( +
+
+
+ +
+
+ + + + +
+
+
+ {moduleViewOptions.map((option) => ( + {replaceUnderscoreIfSnakeCase(option.type)} Layout} + position="bottom" + > + + + ))} + +
+
+ ); +}; diff --git a/web/components/headers/profile-preferences.tsx b/web/components/headers/profile-preferences.tsx new file mode 100644 index 000000000..bef0d4efa --- /dev/null +++ b/web/components/headers/profile-preferences.tsx @@ -0,0 +1,31 @@ +import { useRouter } from "next/router"; +import { ArrowLeft } from "lucide-react"; +// components +import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; + +export const ProfilePreferencesHeader = () => { + const router = useRouter(); + + return ( +
+
+
+ +
+
+ + + +
+
+
+ ); +}; diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 265be968e..2cf5ad93f 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -1,23 +1,24 @@ -import { useCallback, useState } from "react"; +import { useCallback, useState, FC } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; - -// mobx store +import { ArrowLeft, Plus } from "lucide-react"; +// hooks import { useMobxStore } from "lib/mobx/store-provider"; // components import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; import { ProjectAnalyticsModal } from "components/analytics"; // ui import { Button } from "@plane/ui"; -// icons -import { Plus } from "lucide-react"; +import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +// helper +import { truncateText } from "helpers/string.helper"; -export const ProjectIssuesHeader: React.FC = observer(() => { +export const ProjectIssuesHeader: FC = observer(() => { const [analyticsModal, setAnalyticsModal] = useState(false); const router = useRouter(); @@ -100,64 +101,85 @@ export const ProjectIssuesHeader: React.FC = observer(() => { onClose={() => setAnalyticsModal(false)} projectDetails={projectDetails ?? undefined} /> -
- handleLayoutChange(layout)} - selectedLayout={activeLayout} - /> - - m.member)} - states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} +
+
+
+ +
+
+ + + + +
+
+
+ handleLayoutChange(layout)} + selectedLayout={activeLayout} /> - - - - - {projectId && inboxStore.isInboxEnabled(projectId.toString()) && ( - - - - - - )} - - + + m.member)} + states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + /> + + + + + {projectId && inboxStore.isInboxEnabled(projectId.toString()) && ( + + + + + + )} + + +
); diff --git a/web/components/headers/project-views.tsx b/web/components/headers/project-views.tsx index 527402913..dc6f53d70 100644 --- a/web/components/headers/project-views.tsx +++ b/web/components/headers/project-views.tsx @@ -1,30 +1,66 @@ -import { useState } from "react"; - +import { FC, useState } from "react"; +import { useRouter } from "next/router"; +// icons +import { ArrowLeft, Plus } from "lucide-react"; // components import { CreateUpdateProjectViewModal } from "components/views"; +// components +import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs"; // ui import { PrimaryButton } from "components/ui"; -// icons -import { Plus } from "lucide-react"; +// helpers +import { truncateText } from "helpers/string.helper"; -export const ProjectViewsHeader = () => { +interface IProjectViewsHeader { + title: string | undefined; +} + +export const ProjectViewsHeader: FC = (props) => { + const { title } = props; + // router + const router = useRouter(); + const { workspaceSlug } = router.query; + // states const [createViewModal, setCreateViewModal] = useState(false); return ( <> setCreateViewModal(false)} /> -
- { - const e = new KeyboardEvent("keydown", { key: "v" }); - document.dispatchEvent(e); - }} - > - - Create View - +
+
+
+ +
+
+ + + + +
+
+
+
+ { + const e = new KeyboardEvent("keydown", { key: "v" }); + document.dispatchEvent(e); + }} + > + + Create View + +
+
); diff --git a/web/components/icons/module-icon.tsx b/web/components/icons/module-icon.tsx index dbe58eb53..c79a0b9bf 100644 --- a/web/components/icons/module-icon.tsx +++ b/web/components/icons/module-icon.tsx @@ -2,12 +2,7 @@ import React from "react"; import type { Props } from "./types"; -export const ModuleIcon: React.FC = ({ - width = "24", - height = "24", - className, - color = "#F15B5B", -}) => ( +export const ModuleIcon: React.FC = ({ width = "24", height = "24", className, color = "#F15B5B" }) => ( = ({ @@ -27,7 +22,7 @@ export const ModuleIcon: React.FC = ({ d="M5.84925 4.66667H4.81221C4.73039 4.66667 4.66406 4.733 4.66406 4.81482V5.85185C4.66406 5.93367 4.73039 6 4.81221 6H5.84925C5.93107 6 5.9974 5.93367 5.9974 5.85185V4.81482C5.9974 4.733 5.93107 4.66667 5.84925 4.66667Z" fill={color} stroke="#F15B5B" - stroke-width="1.5" + strokeWidth="1.5" stroke-linecap="round" stroke-linejoin="round" /> @@ -35,7 +30,7 @@ export const ModuleIcon: React.FC = ({ d="M5.84925 10H4.81221C4.73039 10 4.66406 10.0663 4.66406 10.1481V11.1852C4.66406 11.267 4.73039 11.3333 4.81221 11.3333H5.84925C5.93107 11.3333 5.9974 11.267 5.9974 11.1852V10.1481C5.9974 10.0663 5.93107 10 5.84925 10Z" fill={color} stroke="#F15B5B" - stroke-width="1.5" + strokeWidth="1.5" stroke-linecap="round" stroke-linejoin="round" /> @@ -43,7 +38,7 @@ export const ModuleIcon: React.FC = ({ d="M11.1852 4.66667H10.1481C10.0663 4.66667 10 4.733 10 4.81482V5.85185C10 5.93367 10.0663 6 10.1481 6H11.1852C11.267 6 11.3333 5.93367 11.3333 5.85185V4.81482C11.3333 4.733 11.267 4.66667 11.1852 4.66667Z" fill={color} stroke="#F15B5B" - stroke-width="1.5" + strokeWidth="1.5" stroke-linecap="round" stroke-linejoin="round" /> @@ -51,7 +46,7 @@ export const ModuleIcon: React.FC = ({ d="M11.1852 10H10.1481C10.0663 10 10 10.0663 10 10.1481V11.1852C10 11.267 10.0663 11.3333 10.1481 11.3333H11.1852C11.267 11.3333 11.3333 11.267 11.3333 11.1852V10.1481C11.3333 10.0663 11.267 10 11.1852 10Z" fill={color} stroke="#F15B5B" - stroke-width="1.5" + strokeWidth="1.5" stroke-linecap="round" stroke-linejoin="round" /> diff --git a/web/constants/themes.ts b/web/constants/themes.ts index 74b74caba..96ece0600 100644 --- a/web/constants/themes.ts +++ b/web/constants/themes.ts @@ -1,6 +1,17 @@ export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"]; -export const THEMES_OBJ = [ +export interface I_THEME_OPTION { + value: string; + label: string; + type: string; + icon: { + border: string; + color1: string; + color2: string; + }; +} + +export const THEME_OPTIONS: I_THEME_OPTION[] = [ { value: "system", label: "System Preference", diff --git a/web/layouts/app-layout/layout.tsx b/web/layouts/app-layout/layout.tsx index cef680116..442e69895 100644 --- a/web/layouts/app-layout/layout.tsx +++ b/web/layouts/app-layout/layout.tsx @@ -1,6 +1,6 @@ import { FC, ReactNode } from "react"; // layouts -import { UserAuthWrapper, WorkspaceAuthWrapper } from "layouts/auth-layout"; +import { UserAuthWrapper, WorkspaceAuthWrapper, ProjectAuthWrapper } from "layouts/auth-layout"; // components import { CommandPalette } from "components/command-palette"; import { AppSidebar } from "./sidebar"; @@ -8,10 +8,11 @@ import { AppSidebar } from "./sidebar"; export interface IAppLayout { children: ReactNode; header: ReactNode; + withProjectWrapper?: boolean; } export const AppLayout: FC = (props) => { - const { children, header } = props; + const { children, header, withProjectWrapper = false } = props; return ( <> @@ -21,10 +22,11 @@ export const AppLayout: FC = (props) => {
- {/*
{header}
*/} {header}
-
{children}
+
+ {withProjectWrapper ? {children} : <>{children}} +
diff --git a/web/layouts/auth-layout/index.ts b/web/layouts/auth-layout/index.ts index 9f8f435fe..fcd53c5eb 100644 --- a/web/layouts/auth-layout/index.ts +++ b/web/layouts/auth-layout/index.ts @@ -1,2 +1,3 @@ export * from "./user-wrapper"; export * from "./workspace-wrapper"; +export * from "./project-wrapper"; diff --git a/web/layouts/auth-layout/project-wrapper.tsx b/web/layouts/auth-layout/project-wrapper.tsx new file mode 100644 index 000000000..1ae3e71f6 --- /dev/null +++ b/web/layouts/auth-layout/project-wrapper.tsx @@ -0,0 +1,91 @@ +import { FC, ReactNode } from "react"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +// hooks +import { useMobxStore } from "lib/mobx/store-provider"; +// components +import { Spinner } from "@plane/ui"; +import { JoinProject } from "components/auth-screens"; +import { EmptyState } from "components/common"; +// images +import emptyProject from "public/empty-state/project.svg"; + +interface IProjectAuthWrapper { + children: ReactNode; +} + +export const ProjectAuthWrapper: FC = (props) => { + const { children } = props; + // store + const { user: userStore, project: projectStore } = useMobxStore(); + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // fetching user project member information + useSWR( + workspaceSlug && projectId ? `PROJECT_MEMBERS_ME_${workspaceSlug}_${projectId}` : null, + workspaceSlug && projectId + ? () => userStore.fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) + : null + ); + // fetching project labels + useSWR( + workspaceSlug && projectId ? `PROJECT_LABELS_${workspaceSlug}_${projectId}` : null, + workspaceSlug && projectId + ? () => projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) + : null + ); + // fetching project members + useSWR( + workspaceSlug && projectId ? `PROJECT_MEMBERS_${workspaceSlug}_${projectId}` : null, + workspaceSlug && projectId + ? () => projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) + : null + ); + // fetching project states + useSWR( + workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null, + workspaceSlug && projectId + ? () => projectStore.fetchProjectStates(workspaceSlug.toString(), projectId.toString()) + : null + ); + + // check if the project member apis is loading + if (!userStore.projectMemberInfo && userStore.hasPermissionToProject === null) { + return ( +
+
+ +
+
+ ); + } + + // check if the user don't have permission to access the project + if (userStore.hasPermissionToProject === false && !userStore.projectNotFound) { + ; + } + + // check if the project info is not found. + if (userStore.hasPermissionToProject === false && userStore.projectNotFound) { +
+ { + const e = new KeyboardEvent("keydown", { + key: "p", + }); + document.dispatchEvent(e); + }, + }} + /> +
; + } + + return <>{children}; +}; diff --git a/web/layouts/auth-layout/user-wrapper.tsx b/web/layouts/auth-layout/user-wrapper.tsx index c3d29aeb5..f839bae0a 100644 --- a/web/layouts/auth-layout/user-wrapper.tsx +++ b/web/layouts/auth-layout/user-wrapper.tsx @@ -5,22 +5,23 @@ import useSWR from "swr"; import { UserService } from "services/user.service"; // ui import { Spinner } from "@plane/ui"; -// fetch-keys -import { CURRENT_USER } from "constants/fetch-keys"; +// store +import { useMobxStore } from "lib/mobx/store-provider"; export interface IUserAuthWrapper { children: ReactNode; } -// services -const userService = new UserService(); - export const UserAuthWrapper: FC = (props) => { const { children } = props; + // store + const { user: userStore } = useMobxStore(); // router const router = useRouter(); // fetching user information - const { data: currentUser, error } = useSWR(CURRENT_USER, () => userService.currentUser()); + const { data: currentUser, error } = useSWR("CURRENT_USER", () => userStore.fetchCurrentUser()); + // fetching user settings + useSWR("CURRENT_USER_SETTINGS", () => userStore.fetchCurrentUserSettings()); if (!currentUser && !error) { return ( diff --git a/web/layouts/auth-layout/workspace-wrapper.tsx b/web/layouts/auth-layout/workspace-wrapper.tsx index bc120dd21..2de91b1dd 100644 --- a/web/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/layouts/auth-layout/workspace-wrapper.tsx @@ -10,11 +10,6 @@ import { useMobxStore } from "lib/mobx/store-provider"; export interface IWorkspaceAuthWrapper { children: ReactNode; - noHeader?: boolean; - bg?: "primary" | "secondary"; - breadcrumbs?: JSX.Element; - left?: JSX.Element; - right?: JSX.Element; } export const WorkspaceAuthWrapper: FC = observer((props) => { @@ -45,12 +40,8 @@ export const WorkspaceAuthWrapper: FC = observer((props) workspaceSlug ? () => workspaceStore.fetchWorkspaceLabels(workspaceSlug.toString()) : null ); - // console.log("workspaceSlug", workspaceSlug); - - // console.log("userStore.memberInfo", userStore.memberInfo); - // while data is being loaded - if (!userStore.memberInfo && userStore.hasPermissionToWorkspace === null) { + if (!userStore.workspaceMemberInfo && userStore.hasPermissionToWorkspace === null) { return (
diff --git a/web/lib/mobx/store-init.tsx b/web/lib/mobx/store-init.tsx index 4624b6502..0c8a71e54 100644 --- a/web/lib/mobx/store-init.tsx +++ b/web/lib/mobx/store-init.tsx @@ -1,11 +1,13 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; // next themes import { useTheme } from "next-themes"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; import { useRouter } from "next/router"; +import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper"; +import { observer } from "mobx-react-lite"; -const MobxStoreInit = () => { +const MobxStoreInit = observer(() => { const { theme: themeStore, user: userStore, @@ -16,12 +18,16 @@ const MobxStoreInit = () => { projectViews: projectViewsStore, inbox: inboxStore, } = useMobxStore(); + // state + const [dom, setDom] = useState(); // theme const { setTheme } = useTheme(); // router const router = useRouter(); const { workspaceSlug, projectId, moduleId, globalViewId, viewId, inboxId } = router.query; + // const dom = useMemo(() => window && window.document?.querySelector("[data-theme='custom']"), [document]); + useEffect(() => { // sidebar collapsed toggle if (localStorage && localStorage.getItem("app_sidebar_collapsed") && themeStore?.sidebarCollapsed === null) @@ -32,20 +38,20 @@ const MobxStoreInit = () => { : false : false ); - - // theme - if (themeStore.theme === null && userStore?.currentUserSettings) { - let currentTheme = localStorage.getItem("theme"); - currentTheme = currentTheme && currentTheme != "undefined" ? currentTheme : "system"; - - // validating the theme and applying for initial state - if (currentTheme) { - setTheme(currentTheme); - themeStore.setTheme({ theme: { theme: currentTheme } }); - } - } }, [themeStore, userStore, setTheme]); + useEffect(() => { + if (!userStore.currentUser) return; + if (window) { + setDom(window.document?.querySelector("[data-theme='custom']")); + } + setTheme(userStore.currentUser?.theme?.theme || "system"); + if (userStore.currentUser?.theme?.theme === "custom" && dom) { + console.log("userStore.currentUser?.theme?.theme", userStore.currentUser?.theme); + applyTheme(userStore.currentUser?.theme?.palette, false); + } else unsetCustomCssVariables(); + }, [userStore.currentUser, setTheme, dom]); + useEffect(() => { if (workspaceSlug) workspaceStore.setWorkspaceSlug(workspaceSlug.toString()); if (projectId) projectStore.setProjectId(projectId.toString()); @@ -69,6 +75,6 @@ const MobxStoreInit = () => { ]); return <>; -}; +}); export default MobxStoreInit; diff --git a/web/pages/[workspaceSlug]/me/profile/preferences.tsx b/web/pages/[workspaceSlug]/me/profile/preferences.tsx index c936d9a31..d08a370cd 100644 --- a/web/pages/[workspaceSlug]/me/profile/preferences.tsx +++ b/web/pages/[workspaceSlug]/me/profile/preferences.tsx @@ -1,92 +1,82 @@ import { useEffect, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { useTheme } from "next-themes"; // hooks -import useUserAuth from "hooks/use-user-auth"; +import { useMobxStore } from "lib/mobx/store-provider"; +import useToast from "hooks/use-toast"; // layouts -import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy"; +import { AppLayout } from "layouts/app-layout"; // components import { CustomThemeSelector, ThemeSwitch } from "components/core"; +import { SettingsSidebar } from "components/project"; +import { ProfilePreferencesHeader } from "components/headers"; // ui import { Spinner } from "@plane/ui"; -import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; -// types -import { ICustomTheme } from "types"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -import { SettingsSidebar } from "components/project"; +// constants +import { I_THEME_OPTION, THEME_OPTIONS } from "constants/themes"; -const ProfilePreferences = observer(() => { - const { user: myProfile } = useUserAuth(); - - const store: any = useMobxStore(); - - // console.log("store", store?.theme?.theme); - // console.log("theme", theme); - - const [customThemeSelectorOptions, setCustomThemeSelectorOptions] = useState(false); - - const [preLoadedData, setPreLoadedData] = useState(null); +const ProfilePreferencesPage = observer(() => { + const { user: userStore } = useMobxStore(); + // states + const [currentTheme, setCurrentTheme] = useState(null); + // computed + const userTheme = userStore.currentUser?.theme; + // hooks + const { setTheme } = useTheme(); + const { setToastAlert } = useToast(); useEffect(() => { - if (store?.user && store?.theme?.theme === "custom") { - const currentTheme = store?.user?.currentUserSettings?.theme; - if (currentTheme.palette) - setPreLoadedData({ - background: currentTheme.background !== "" ? currentTheme.background : "#0d101b", - text: currentTheme.text !== "" ? currentTheme.text : "#c5c5c5", - primary: currentTheme.primary !== "" ? currentTheme.primary : "#3f76ff", - sidebarBackground: currentTheme.sidebarBackground !== "" ? currentTheme.sidebarBackground : "#0d101b", - sidebarText: currentTheme.sidebarText !== "" ? currentTheme.sidebarText : "#c5c5c5", - darkPalette: false, - palette: currentTheme.palette !== ",,,," ? currentTheme.palette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5", - theme: "custom", - }); - setCustomThemeSelectorOptions(() => true); + if (userTheme) { + const userThemeOption = THEME_OPTIONS.find((t) => t.value === userTheme?.theme); + if (userThemeOption) { + setCurrentTheme(userThemeOption); + } } - }, [store, store?.theme?.theme]); + }, [userTheme]); + + const handleThemeChange = (themeOption: I_THEME_OPTION) => { + setTheme(themeOption.value); + userStore.updateCurrentUserTheme(themeOption.value).catch(() => { + setToastAlert({ + title: "Failed to Update the theme", + type: "error", + }); + }); + }; return ( - - - - } - > - {myProfile ? ( -
-
- -
+ }> + <> + {userStore.currentUser ? ( +
+
+ +
-
-
-

Preferences

-
-
-
-

Theme

-

Select or customize your interface color scheme.

+
+
+

Preferences

-
- +
+
+

Theme

+

Select or customize your interface color scheme.

+
+
+ +
+ {userTheme?.theme === "custom" && }
- {customThemeSelectorOptions && }
-
- ) : ( -
- -
- )} - + ) : ( +
+ +
+ )} + + ); }); -export default ProfilePreferences; +export default ProfilePreferencesPage; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index 225d1b8c4..eb2d07dfa 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -2,25 +2,23 @@ import { Fragment, useCallback, useEffect, useState } from "react"; import { useRouter } from "next/router"; import { Tab } from "@headlessui/react"; import useSWR from "swr"; +import { observer } from "mobx-react-lite"; import { Plus } from "lucide-react"; +// hooks +import { useMobxStore } from "lib/mobx/store-provider"; // layouts -import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; +import { AppLayout } from "layouts/app-layout"; // components +import { CyclesHeader } from "components/headers"; import { CyclesView, ActiveCycleDetails } from "components/cycles"; import { CycleCreateEditModal } from "components/cycles/cycle-create-edit-modal"; // ui -import { Button } from "@plane/ui"; import { EmptyState } from "components/common"; -import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // images import emptyCycle from "public/empty-state/cycle.svg"; // types import { TCycleView, TCycleLayout } from "types"; import type { NextPage } from "next"; -// helper -import { truncateText } from "helpers/string.helper"; -import { useMobxStore } from "lib/mobx/store-provider"; -import { observer } from "mobx-react-lite"; // constants import { CYCLE_TAB_LIST, CYCLE_VIEWS } from "constants/cycle"; // lib cookie @@ -85,25 +83,7 @@ const ProjectCyclesPage: NextPage = observer(() => { const cycleLayout = cycleStore?.cycleLayout; return ( - - - - - } - right={ - - } - > + } withProjectWrapper> { )} - + ); }); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx index be49791a4..96652b159 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/inbox/[inboxId].tsx @@ -1,7 +1,7 @@ import { useRouter } from "next/router"; +import { NextPage } from "next"; import useSWR from "swr"; - -// mobx store +// hooks import { useMobxStore } from "lib/mobx/store-provider"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; @@ -12,8 +12,6 @@ import { ProjectInboxHeader } from "components/headers"; import { truncateText } from "helpers/string.helper"; // ui import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; -// types -import type { NextPage } from "next"; const ProjectInbox: NextPage = () => { const router = useRouter(); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx index c68dad900..80e85a74e 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/issues/index.tsx @@ -1,38 +1,20 @@ import { useRouter } from "next/router"; - import useSWR from "swr"; - // mobx import { useMobxStore } from "lib/mobx/store-provider"; -// services -import { ProjectService } from "services/project"; -// layouts -import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; -// helper -import { truncateText } from "helpers/string.helper"; // components import { ProjectLayoutRoot } from "components/issues"; import { ProjectIssuesHeader } from "components/headers"; -// ui -import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // types import type { NextPage } from "next"; -// fetch-keys -import { PROJECT_DETAILS } from "constants/fetch-keys"; - -// services -const projectService = new ProjectService(); +// layouts +import { AppLayout } from "layouts/app-layout"; const ProjectIssues: NextPage = () => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { issueFilter: issueFilterStore, project: projectStore, inbox: inboxStore } = useMobxStore(); - - const { data: projectDetails } = useSWR( - workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, - workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null - ); + const { issueFilter: issueFilterStore } = useMobxStore(); // TODO: update the fetch keys useSWR( @@ -42,57 +24,12 @@ const ProjectIssues: NextPage = () => { : null ); - useSWR( - workspaceSlug && projectId ? "REVALIDATE_PROJECT_STATES_LIST" : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectStates(workspaceSlug.toString(), projectId.toString()) - : null - ); - - useSWR( - workspaceSlug && projectId ? "REVALIDATE_PROJECT_LABELS_LIST" : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) - : null - ); - - useSWR( - workspaceSlug && projectId ? "REVALIDATE_PROJECT_MEMBERS_LIST" : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) - : null - ); - - useSWR( - workspaceSlug && projectId ? "REVALIDATE_PROJECT_DETAILS" : null, - workspaceSlug && projectId - ? () => projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) - : null - ); - - useSWR( - workspaceSlug && projectId ? "REVALIDATE_INBOXES_LIST" : null, - workspaceSlug && projectId - ? () => inboxStore.fetchInboxesList(workspaceSlug.toString(), projectId.toString()) - : null - ); - // TODO: remove all the above fetching logic to project auth wrapper - return ( - - - - - } - right={} - bg="secondary" - > + } withProjectWrapper>
-
+ ); }; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx index a2c4cc1c3..816318ceb 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/index.tsx @@ -1,11 +1,9 @@ import React, { useEffect, useState } from "react"; - import { useRouter } from "next/router"; - import useSWR from "swr"; - +import { NextPage } from "next"; // layouts -import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; +import { AppLayout } from "layouts/app-layout"; // hooks import useUserAuth from "hooks/use-user-auth"; // services @@ -13,45 +11,30 @@ import { ProjectService } from "services/project"; import { ModuleService } from "services/module.service"; // components import { CreateUpdateModuleModal, ModulesListGanttChartView, SingleModuleCard } from "components/modules"; +import { ModulesHeader } from "components/headers"; // ui -import { Button, Loader, Tooltip } from "@plane/ui"; +import { Loader } from "@plane/ui"; import { EmptyState } from "components/common"; -import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons -import { GanttChart, LayoutGrid, Plus } from "lucide-react"; +import { Plus } from "lucide-react"; // images import emptyModule from "public/empty-state/module.svg"; // types import { IModule, SelectModuleType } from "types/modules"; -import type { NextPage } from "next"; // fetch-keys import { MODULE_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; -// helper -import { replaceUnderscoreIfSnakeCase, truncateText } from "helpers/string.helper"; - -const moduleViewOptions: { type: "grid" | "gantt_chart"; icon: any }[] = [ - { - type: "gantt_chart", - icon: GanttChart, - }, - { - type: "grid", - icon: LayoutGrid, - }, -]; // services const projectService = new ProjectService(); const moduleService = new ModuleService(); const ProjectModules: NextPage = () => { - const [selectedModule, setSelectedModule] = useState(); - const [createUpdateModule, setCreateUpdateModule] = useState(false); - - const [modulesView, setModulesView] = useState<"grid" | "gantt_chart">("grid"); - const router = useRouter(); const { workspaceSlug, projectId } = router.query; + // states + const [modulesView, setModulesView] = useState<"grid" | "gantt_chart">("grid"); + const [selectedModule, setSelectedModule] = useState(); + const [createUpdateModule, setCreateUpdateModule] = useState(false); const { user } = useUserAuth(); @@ -80,44 +63,8 @@ const ProjectModules: NextPage = () => { }, [createUpdateModule]); return ( - - - - - } - right={ -
- {moduleViewOptions.map((option) => ( - {replaceUnderscoreIfSnakeCase(option.type)} Layout} - position="bottom" - > - - - ))} - -
- } + } > { )} -
+ ); }; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index 6bb245e2d..aad472e75 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -1,21 +1,14 @@ import React from "react"; - +import type { NextPage } from "next"; import { useRouter } from "next/router"; - import useSWR from "swr"; - -// mobx store +// hooks import { useMobxStore } from "lib/mobx/store-provider"; -// layouts -import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; // components import { ProjectViewsHeader } from "components/headers"; -// ui -import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; -// components import { ProjectViewsList } from "components/views"; -// types -import type { NextPage } from "next"; +// layouts +import { AppLayout } from "layouts/app-layout"; const ProjectViews: NextPage = () => { const router = useRouter(); @@ -43,17 +36,9 @@ const ProjectViews: NextPage = () => { : undefined; return ( - - - - - } - right={} - > + }> - + ); }; diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index 23dbf0b52..48535993a 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -342,13 +342,12 @@ export class ProjectStore implements IProjectStore { this.error = null; const labelResponse = await this.issueLabelService.getProjectIssueLabels(workspaceSlug, projectId); - const _labels = { - ...this.labels, - [projectId]: labelResponse, - }; runInAction(() => { - this.labels = _labels; + this.labels = { + ...this.labels, + [projectId]: labelResponse, + }; this.loader = false; this.error = null; }); diff --git a/web/store/user.store.ts b/web/store/user.store.ts index e37696159..2e4e7d13b 100644 --- a/web/store/user.store.ts +++ b/web/store/user.store.ts @@ -1,35 +1,58 @@ // mobx import { action, observable, runInAction, makeObservable } from "mobx"; // services +import { ProjectService } from "services/project"; import { UserService } from "services/user.service"; import { WorkspaceService } from "services/workspace.service"; // interfaces import { IUser, IUserSettings } from "types/users"; +import { IWorkspaceMember, IProjectMember } from "types"; interface IUserStore { loader: boolean; + currentUser: IUser | null; currentUserSettings: IUserSettings | null; + dashboardInfo: any; - memberInfo: any; + + workspaceMemberInfo: any; hasPermissionToWorkspace: boolean | null; + + projectMemberInfo: any; + projectNotFound: boolean; + hasPermissionToProject: boolean | null; + fetchCurrentUser: () => Promise; fetchCurrentUserSettings: () => Promise; + + fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise; + fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise; + updateTourCompleted: () => Promise; + updateCurrentUser: (data: Partial) => Promise; } class UserStore implements IUserStore { loader: boolean = false; + currentUser: IUser | null = null; currentUserSettings: IUserSettings | null = null; + dashboardInfo: any = null; - memberInfo: any = null; + + workspaceMemberInfo: any = null; hasPermissionToWorkspace: boolean | null = null; + + projectMemberInfo: any = null; + projectNotFound: boolean = false; + hasPermissionToProject: boolean | null = null; // root store rootStore; // services userService; workspaceService; + projectService; constructor(_rootStore: any) { makeObservable(this, { @@ -38,8 +61,11 @@ class UserStore implements IUserStore { currentUser: observable.ref, currentUserSettings: observable.ref, dashboardInfo: observable.ref, - memberInfo: observable.ref, + workspaceMemberInfo: observable.ref, hasPermissionToWorkspace: observable.ref, + projectMemberInfo: observable.ref, + projectNotFound: observable.ref, + hasPermissionToProject: observable.ref, // action fetchCurrentUser: action, fetchCurrentUserSettings: action, @@ -48,6 +74,7 @@ class UserStore implements IUserStore { this.rootStore = _rootStore; this.userService = new UserService(); this.workspaceService = new WorkspaceService(); + this.projectService = new ProjectService(); } fetchCurrentUser = async () => { @@ -94,7 +121,7 @@ class UserStore implements IUserStore { try { const response = await this.workspaceService.workspaceMemberMe(workspaceSlug.toString()); runInAction(() => { - this.memberInfo = response; + this.workspaceMemberInfo = response; this.hasPermissionToWorkspace = true; }); return response; @@ -106,6 +133,29 @@ class UserStore implements IUserStore { } }; + fetchUserProjectInfo = async (workspaceSlug: string, projectId: string) => { + try { + const response = await this.projectService.projectMemberMe(workspaceSlug, projectId); + + console.log("response", response); + runInAction(() => { + this.projectMemberInfo = response; + this.hasPermissionToWorkspace = true; + }); + return response; + } catch (error: any) { + runInAction(() => { + this.hasPermissionToWorkspace = false; + }); + if (error?.status === 404) { + runInAction(() => { + this.projectNotFound = true; + }); + } + throw error; + } + }; + updateTourCompleted = async () => { try { if (this.currentUser) { @@ -123,107 +173,37 @@ class UserStore implements IUserStore { } }; - // setCurrentUser = async () => { - // try { - // let userResponse: ICurrentUser | null = await UserService.currentUser(); - // userResponse = userResponse || null; + updateCurrentUser = async (data: Partial) => { + try { + const response = await this.userService.updateUser(data); + runInAction(() => { + this.currentUser = response; + }); + return response; + } catch (error) { + throw error; + } + }; - // if (userResponse) { - // const userPayload: ICurrentUser = { - // id: userResponse?.id, - // avatar: userResponse?.avatar, - // first_name: userResponse?.first_name, - // last_name: userResponse?.last_name, - // username: userResponse?.username, - // email: userResponse?.email, - // mobile_number: userResponse?.mobile_number, - // is_email_verified: userResponse?.is_email_verified, - // is_tour_completed: userResponse?.is_tour_completed, - // onboarding_step: userResponse?.onboarding_step, - // is_onboarded: userResponse?.is_onboarded, - // role: userResponse?.role, - // }; - // runInAction(() => { - // this.currentUser = userPayload; - // }); - // } - // } catch (error) { - // console.error("Fetching current user error", error); - // } - // }; - - // setCurrentUserSettings = async () => { - // try { - // let userSettingsResponse: ICurrentUserSettings | null = await UserService.currentUser(); - // userSettingsResponse = userSettingsResponse || null; - - // if (userSettingsResponse) { - // const themePayload = { - // theme: { ...userSettingsResponse?.theme }, - // }; - // runInAction(() => { - // this.currentUserSettings = themePayload; - // this.rootStore.theme.setTheme(themePayload); - // }); - // } - // } catch (error) { - // console.error("Fetching current user error", error); - // } - // }; - - // updateCurrentUser = async (user: ICurrentUser) => { - // try { - // let userResponse: ICurrentUser = await UserService.updateUser(user); - // userResponse = userResponse || null; - - // if (userResponse) { - // const userPayload: ICurrentUser = { - // id: userResponse?.id, - // avatar: userResponse?.avatar, - // first_name: userResponse?.first_name, - // last_name: userResponse?.last_name, - // username: userResponse?.username, - // email: userResponse?.email, - // mobile_number: userResponse?.mobile_number, - // is_email_verified: userResponse?.is_email_verified, - // is_tour_completed: userResponse?.is_tour_completed, - // onboarding_step: userResponse?.onboarding_step, - // is_onboarded: userResponse?.is_onboarded, - // role: userResponse?.role, - // }; - // runInAction(() => { - // this.currentUser = userPayload; - // }); - // return userPayload; - // } - // } catch (error) { - // console.error("Updating user error", error); - // return error; - // } - // }; - - // updateCurrentUserSettings = async (userTheme: ICurrentUserSettings) => { - // try { - // let userSettingsResponse: ICurrentUserSettings = await UserService.updateUser(userTheme); - // userSettingsResponse = userSettingsResponse || null; - // if (userSettingsResponse) { - // const themePayload = { - // theme: { ...userSettingsResponse?.theme }, - // }; - // runInAction(() => { - // this.currentUserSettings = themePayload; - // this.rootStore.theme.setTheme(themePayload); - // }); - // return themePayload; - // } - // } catch (error) { - // console.error("Updating user settings error", error); - // return error; - // } - // }; - - // init load - initialLoad() {} + updateCurrentUserTheme = async (theme: string) => { + try { + runInAction(() => { + this.currentUser = { + ...this.currentUser, + theme: { + ...this.currentUser?.theme, + theme, + }, + } as IUser; + }); + const response = await this.userService.updateUser({ + theme: { ...this.currentUser?.theme, theme }, + } as IUser); + return response; + } catch (error) { + throw error; + } + }; } export default UserStore; diff --git a/web/types/users.d.ts b/web/types/users.d.ts index c80160e3d..6ffeedf8b 100644 --- a/web/types/users.d.ts +++ b/web/types/users.d.ts @@ -26,7 +26,7 @@ export interface IUser { last_workspace_id: string; user_timezone: string; username: string; - theme: ICustomTheme; + theme: IUserTheme; } export interface IUserSettings { @@ -41,7 +41,7 @@ export interface IUserSettings { }; } -export interface ICustomTheme { +export interface IUserTheme { background: string; text: string; primary: string;