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
This commit is contained in:
sriram veeraghanta 2023-10-17 12:46:38 +05:30 committed by GitHub
parent e496cec49f
commit 98b1a078de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 891 additions and 699 deletions

View File

@ -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"
}
}

View File

@ -1,2 +1,3 @@
export * from "./input";
export * from "./textarea";
export * from "./input-color-picker"

View File

@ -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<InputColorPickerProps> = (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 (
<div className="relative">
<Input
id={name}
name={name}
type="text"
value={value}
onChange={handleInputChange}
hasError={hasError}
placeholder={placeholder}
className={className}
/>
<div className="absolute right-4 top-2.5">
<Popover className="relative grid place-items-center">
{({ open }) => (
<>
<Popover.Button
type="button"
className={`group inline-flex items-center outline-none ${
open ? "text-custom-text-100" : "text-custom-text-200"
}`}
>
{value && value !== "" ? (
<span
className="h-4 w-4 rounded border border-custom-border-200"
style={{
backgroundColor: `${value}`,
}}
/>
) : (
<svg
width={14}
height={14}
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0.8125 13.7508C0.65 13.7508 0.515625 13.6977 0.409375 13.5914C0.303125 13.4852 0.25 13.3508 0.25 13.1883V10.8258C0.25 10.7508 0.2625 10.682 0.2875 10.6195C0.3125 10.557 0.35625 10.4945 0.41875 10.432L7.31875 3.53203L6.34375 2.55703C6.24375 2.45703 6.19688 2.32891 6.20312 2.17266C6.20938 2.01641 6.2625 1.88828 6.3625 1.78828C6.4625 1.68828 6.59063 1.63828 6.74688 1.63828C6.90313 1.63828 7.03125 1.68828 7.13125 1.78828L8.4625 3.13828L11.125 0.475781C11.2625 0.338281 11.4094 0.269531 11.5656 0.269531C11.7219 0.269531 11.8688 0.338281 12.0063 0.475781L13.525 1.99453C13.6625 2.13203 13.7313 2.27891 13.7313 2.43516C13.7313 2.59141 13.6625 2.73828 13.525 2.87578L10.8625 5.53828L12.2125 6.88828C12.3125 6.98828 12.3625 7.11328 12.3625 7.26328C12.3625 7.41328 12.3125 7.53828 12.2125 7.63828C12.1125 7.73828 11.9844 7.78828 11.8281 7.78828C11.6719 7.78828 11.5438 7.73828 11.4438 7.63828L10.4688 6.68203L3.56875 13.582C3.50625 13.6445 3.44375 13.6883 3.38125 13.7133C3.31875 13.7383 3.25 13.7508 3.175 13.7508H0.8125ZM1.375 12.6258H3.00625L9.6625 5.96953L8.03125 4.33828L1.375 10.9945V12.6258ZM10.0563 4.75078L12.3813 2.42578L11.575 1.61953L9.25 3.94453L10.0563 4.75078Z" />
</svg>
)}
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel
className={`absolute bottom-8 z-20 mt-1 max-w-xs px-2 sm:px-0 right-0`}
>
<SketchPicker color={value} onChange={handleColorChange} />
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
</div>
);
};

View File

@ -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<Props> = observer(({ setIsPaletteOpe
return (
<>
{THEMES_OBJ.filter((t) => t.value !== "custom").map((theme) => (
{THEME_OPTIONS.filter((t) => t.value !== "custom").map((theme) => (
<Command.Item
key={theme.value}
onSelect={() => {

View File

@ -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<any>;
setValue: UseFormSetValue<any>;
control: Control<ICustomTheme, any>;
control: Control<IUserTheme, any>;
error: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
register: UseFormRegister<any>;
};
@ -38,7 +38,7 @@ export const ColorPickerInput: FC<Props> = (props) => {
setValue(name, hex);
};
const getColorText = (colorName: keyof ICustomTheme) => {
const getColorText = (colorName: keyof IUserTheme) => {
switch (colorName) {
case "background":
return "Background";

View File

@ -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<ICustomTheme> | 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<Props> = observer(({ preLoadedData }) => {
const store: any = useMobxStore();
export const CustomThemeSelector: FC<Props> = 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<ICustomTheme>({
defaultValues,
} = 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 : "",
},
});
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<Props> = observer(({ preLoadedData })
<div className="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2 md:grid-cols-3">
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Background color</h3>
<ColorPickerInput
name="background"
position="right"
<Controller
control={control}
error={errors.background}
watch={watch}
setValue={setValue}
register={register}
name="background"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="background"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.background)}
/>
)}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Text color</h3>
<ColorPickerInput
name="text"
<Controller
control={control}
error={errors.text}
watch={watch}
setValue={setValue}
register={register}
name="text"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="text"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.text)}
/>
)}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Primary(Theme) color</h3>
<ColorPickerInput
name="primary"
error={errors.primary}
<Controller
control={control}
watch={watch}
setValue={setValue}
register={register}
name="primary"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="primary"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.primary)}
/>
)}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Sidebar background color</h3>
<ColorPickerInput
name="sidebarBackground"
position="right"
<Controller
control={control}
error={errors.sidebarBackground}
watch={watch}
setValue={setValue}
register={register}
name="sidebarBackground"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="sidebarBackground"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.sidebarBackground)}
/>
)}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Sidebar text color</h3>
<ColorPickerInput
name="sidebarText"
<Controller
control={control}
error={errors.sidebarText}
watch={watch}
setValue={setValue}
register={register}
name="sidebarText"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="sidebarText"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.sidebarText)}
/>
)}
/>
</div>
</div>

View File

@ -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<React.SetStateAction<ICustomTheme | null>>;
customThemeSelectorOptions: boolean;
setCustomThemeSelectorOptions: React.Dispatch<React.SetStateAction<boolean>>;
value: I_THEME_OPTION | null;
onChange: (value: I_THEME_OPTION) => void;
};
export const ThemeSwitch: React.FC<Props> = observer(
({ setPreLoadedData, customThemeSelectorOptions, setCustomThemeSelectorOptions }) => {
const store: any = useMobxStore();
export const ThemeSwitch: FC<Props> = (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 (
<CustomSelect
value={theme}
label={
currentThemeObj ? (
<div className="flex items-center gap-2">
return (
<CustomSelect
value={value}
label={
value ? (
<div className="flex items-center gap-2">
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
style={{
borderColor: value.icon.border,
}}
>
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
className="h-full w-1/2 rounded-l-full"
style={{
borderColor: currentThemeObj.icon.border,
background: value.icon.color1,
}}
>
<div
className="h-full w-1/2 rounded-l-full"
style={{
background: currentThemeObj.icon.color1,
}}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: currentThemeObj.icon.border,
background: currentThemeObj.icon.color2,
}}
/>
</div>
{currentThemeObj.label}
</div>
) : (
"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 }) => (
<CustomSelect.Option key={value} value={{ value, type }}>
<div className="flex items-center gap-2">
/>
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderColor: icon.border,
borderLeftColor: value.icon.border,
background: value.icon.color2,
}}
>
<div
className="h-full w-1/2 rounded-l-full"
style={{
background: icon.color1,
}}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: icon.border,
background: icon.color2,
}}
/>
</div>
{label}
/>
</div>
</CustomSelect.Option>
))}
</CustomSelect>
);
}
);
{value.label}
</div>
) : (
"Select your theme"
)
}
onChange={onChange}
input
width="w-full"
>
{THEME_OPTIONS.map((themeOption) => (
<CustomSelect.Option key={themeOption.value} value={themeOption}>
<div className="flex items-center gap-2">
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
style={{
borderColor: themeOption.icon.border,
}}
>
<div
className="h-full w-1/2 rounded-l-full"
style={{
background: themeOption.icon.color1,
}}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: themeOption.icon.border,
background: themeOption.icon.color2,
}}
/>
</div>
{themeOption.label}
</div>
</CustomSelect.Option>
))}
</CustomSelect>
);
};

View File

@ -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<ICyclesHeader> = (props) => {
const { name } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<div
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
>
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div className="block md:hidden">
<button
type="button"
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
onClick={() => router.back()}
>
<ArrowLeft fontSize={14} strokeWidth={2} />
</button>
</div>
<div>
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${truncateText(name ?? "Project", 32)} Cycles`} />
</Breadcrumbs>
</div>
</div>
<div className="flex items-center gap-3">
<Button
variant="primary"
prependIcon={<Plus />}
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "q" });
document.dispatchEvent(e);
}}
>
Add Cycle
</Button>
</div>
</div>
);
};

View File

@ -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";

View File

@ -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<SetStateAction<"grid" | "gantt_chart">>;
}
const moduleViewOptions: { type: "grid" | "gantt_chart"; icon: any }[] = [
{
type: "gantt_chart",
icon: "view_timeline",
},
{
type: "grid",
icon: "table_rows",
},
];
export const ModulesHeader: FC<IModulesHeader> = (props) => {
const { name, modulesView, setModulesView } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<div
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
>
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div className="block md:hidden">
<button
type="button"
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
onClick={() => router.back()}
>
<ArrowLeft fontSize={14} strokeWidth={2} />
</button>
</div>
<div>
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${truncateText(name ?? "Project", 32)} Cycles`} />
</Breadcrumbs>
</div>
</div>
<div className="flex items-center gap-2">
{moduleViewOptions.map((option) => (
<Tooltip
key={option.type}
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>}
position="bottom"
>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
modulesView === option.type ? "bg-custom-sidebar-background-80" : "text-custom-sidebar-text-200"
}`}
onClick={() => setModulesView(option.type)}
>
<Icon iconName={option.icon} className={`!text-base ${option.type === "grid" ? "rotate-90" : ""}`} />
</button>
</Tooltip>
))}
<Button
variant="primary"
prependIcon={<Plus />}
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "m" });
document.dispatchEvent(e);
}}
>
Add Module
</Button>
</div>
</div>
);
};

View File

@ -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 (
<div
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
>
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div className="block md:hidden">
<button
type="button"
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
onClick={() => router.back()}
>
<ArrowLeft fontSize={14} strokeWidth={2} />
</button>
</div>
<div>
<Breadcrumbs>
<BreadcrumbItem title="My Profile Preferences" />
</Breadcrumbs>
</div>
</div>
</div>
);
};

View File

@ -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}
/>
<div className="flex items-center gap-2">
<LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters">
<FilterSelection
filters={issueFilterStore.userFilters}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
<div
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
>
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div className="block md:hidden">
<button
type="button"
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
onClick={() => router.back()}
>
<ArrowLeft fontSize={14} strokeWidth={2} />
</button>
</div>
<div>
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`} />
</Breadcrumbs>
</div>
</div>
<div className="flex items-center gap-3">
<LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
</FiltersDropdown>
<FiltersDropdown title="View">
<DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
displayProperties={issueFilterStore.userDisplayProperties}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
/>
</FiltersDropdown>
{projectId && inboxStore.isInboxEnabled(projectId.toString()) && (
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxStore.getInboxId(projectId.toString())}`}>
<a>
<Button variant="neutral-primary" size="sm" className="relative">
Inbox
{inboxDetails && (
<span className="absolute -top-1.5 -right-1.5 h-4 w-4 rounded-full text-custom-text-100 bg-custom-sidebar-background-80 border border-custom-sidebar-border-200">
{inboxDetails.pending_issue_count}
</span>
)}
</Button>
</a>
</Link>
)}
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
<Button
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
}}
size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
<FiltersDropdown title="Filters">
<FilterSelection
filters={issueFilterStore.userFilters}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/>
</FiltersDropdown>
<FiltersDropdown title="View">
<DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
displayProperties={issueFilterStore.userDisplayProperties}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
/>
</FiltersDropdown>
{projectId && inboxStore.isInboxEnabled(projectId.toString()) && (
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxStore.getInboxId(projectId.toString())}`}>
<a>
<Button variant="neutral-primary" size="sm" className="relative">
Inbox
{inboxDetails && (
<span className="absolute -top-1.5 -right-1.5 h-4 w-4 rounded-full text-custom-text-100 bg-custom-sidebar-background-80 border border-custom-sidebar-border-200">
{inboxDetails.pending_issue_count}
</span>
)}
</Button>
</a>
</Link>
)}
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
<Button
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
}}
size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
</div>
</div>
</>
);

View File

@ -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<IProjectViewsHeader> = (props) => {
const { title } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// states
const [createViewModal, setCreateViewModal] = useState(false);
return (
<>
<CreateUpdateProjectViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
<div>
<PrimaryButton
type="button"
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "v" });
document.dispatchEvent(e);
}}
>
<Plus size={14} strokeWidth={2} />
Create View
</PrimaryButton>
<div
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
>
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div className="block md:hidden">
<button
type="button"
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
onClick={() => router.back()}
>
<ArrowLeft fontSize={14} strokeWidth={2} />
</button>
</div>
<div>
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${truncateText(title ?? "Project", 32)} Cycles`} />
</Breadcrumbs>
</div>
</div>
<div className="flex items-center gap-2">
<div>
<PrimaryButton
type="button"
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "v" });
document.dispatchEvent(e);
}}
>
<Plus size={14} strokeWidth={2} />
Create View
</PrimaryButton>
</div>
</div>
</div>
</>
);

View File

@ -2,12 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const ModuleIcon: React.FC<Props> = ({
width = "24",
height = "24",
className,
color = "#F15B5B",
}) => (
export const ModuleIcon: React.FC<Props> = ({ width = "24", height = "24", className, color = "#F15B5B" }) => (
<svg
width={width}
height={height}
@ -19,7 +14,7 @@ export const ModuleIcon: React.FC<Props> = ({
<path
d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z"
stroke="#F15B5B"
stroke-width="1.5"
strokeWidth="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
@ -27,7 +22,7 @@ export const ModuleIcon: React.FC<Props> = ({
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<Props> = ({
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<Props> = ({
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<Props> = ({
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"
/>

View File

@ -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",

View File

@ -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<IAppLayout> = (props) => {
const { children, header } = props;
const { children, header, withProjectWrapper = false } = props;
return (
<>
@ -21,10 +22,11 @@ export const AppLayout: FC<IAppLayout> = (props) => {
<div className="relative flex h-screen w-full overflow-hidden">
<AppSidebar />
<main className="relative flex flex-col h-full w-full overflow-hidden bg-custom-background-100">
{/* <div className="relative w-full">{header}</div> */}
{header}
<div className="h-full w-full overflow-hidden">
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">{children}</div>
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
{withProjectWrapper ? <ProjectAuthWrapper>{children}</ProjectAuthWrapper> : <>{children}</>}
</div>
</div>
</main>
</div>

View File

@ -1,2 +1,3 @@
export * from "./user-wrapper";
export * from "./workspace-wrapper";
export * from "./project-wrapper";

View File

@ -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<IProjectAuthWrapper> = (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 (
<div className="grid h-screen place-items-center p-4 bg-custom-background-100">
<div className="flex flex-col items-center gap-3 text-center">
<Spinner />
</div>
</div>
);
}
// check if the user don't have permission to access the project
if (userStore.hasPermissionToProject === false && !userStore.projectNotFound) {
<JoinProject />;
}
// check if the project info is not found.
if (userStore.hasPermissionToProject === false && userStore.projectNotFound) {
<div className="container grid h-screen place-items-center bg-custom-background-100">
<EmptyState
title="No such project exists"
description="Try creating a new project"
image={emptyProject}
primaryButton={{
text: "Create Project",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "p",
});
document.dispatchEvent(e);
},
}}
/>
</div>;
}
return <>{children}</>;
};

View File

@ -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<IUserAuthWrapper> = (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 (

View File

@ -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<IWorkspaceAuthWrapper> = observer((props) => {
@ -45,12 +40,8 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = 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 (
<div className="grid h-screen place-items-center p-4 bg-custom-background-100">
<div className="flex flex-col items-center gap-3 text-center">

View File

@ -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<any>();
// theme
const { setTheme } = useTheme();
// router
const router = useRouter();
const { workspaceSlug, projectId, moduleId, globalViewId, viewId, inboxId } = router.query;
// const dom = useMemo(() => window && window.document?.querySelector<HTMLElement>("[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<HTMLElement>("[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;

View File

@ -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<ICustomTheme | null>(null);
const ProfilePreferencesPage = observer(() => {
const { user: userStore } = useMobxStore();
// states
const [currentTheme, setCurrentTheme] = useState<I_THEME_OPTION | null>(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 (
<WorkspaceAuthorizationLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="My Profile Preferences" />
</Breadcrumbs>
}
>
{myProfile ? (
<div className="flex flex-row gap-2 h-full">
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
<SettingsSidebar />
</div>
<AppLayout header={<ProfilePreferencesHeader />}>
<>
{userStore.currentUser ? (
<div className="flex flex-row gap-2 h-full">
<div className="w-80 pt-8 overflow-y-hidden flex-shrink-0">
<SettingsSidebar />
</div>
<div className="pr-9 py-8 w-full overflow-y-auto">
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Preferences</h3>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16 py-6">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Theme</h4>
<p className="text-sm text-custom-text-200">Select or customize your interface color scheme.</p>
<div className="pr-9 py-8 w-full overflow-y-auto">
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Preferences</h3>
</div>
<div className="col-span-12 sm:col-span-6">
<ThemeSwitch
setPreLoadedData={setPreLoadedData}
customThemeSelectorOptions={customThemeSelectorOptions}
setCustomThemeSelectorOptions={setCustomThemeSelectorOptions}
/>
<div className="grid grid-cols-12 gap-4 sm:gap-16 py-6">
<div className="col-span-12 sm:col-span-6">
<h4 className="text-lg font-semibold text-custom-text-100">Theme</h4>
<p className="text-sm text-custom-text-200">Select or customize your interface color scheme.</p>
</div>
<div className="col-span-12 sm:col-span-6">
<ThemeSwitch value={currentTheme} onChange={handleThemeChange} />
</div>
</div>
{userTheme?.theme === "custom" && <CustomThemeSelector />}
</div>
{customThemeSelectorOptions && <CustomThemeSelector preLoadedData={preLoadedData} />}
</div>
</div>
) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
<Spinner />
</div>
)}
</WorkspaceAuthorizationLayout>
) : (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
<Spinner />
</div>
)}
</>
</AppLayout>
);
});
export default ProfilePreferences;
export default ProfilePreferencesPage;

View File

@ -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 (
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Cycles`} />
</Breadcrumbs>
}
right={
<Button
variant="primary"
prependIcon={<Plus />}
onClick={() => {
setCreateModal(true);
}}
>
Add Cycle
</Button>
}
>
<AppLayout header={<CyclesHeader name={projectDetails?.name} />} withProjectWrapper>
<CycleCreateEditModal
workspaceSlug={workspaceSlug}
projectId={projectId}
@ -226,7 +206,7 @@ const ProjectCyclesPage: NextPage = observer(() => {
</Tab.Panels>
</Tab.Group>
)}
</ProjectAuthorizationWrapper>
</AppLayout>
);
});

View File

@ -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();

View File

@ -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 (
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`} />
</Breadcrumbs>
}
right={<ProjectIssuesHeader />}
bg="secondary"
>
<AppLayout header={<ProjectIssuesHeader />} withProjectWrapper>
<div className="h-full w-full flex flex-col">
<ProjectLayoutRoot />
</div>
</ProjectAuthorizationWrapper>
</AppLayout>
);
};

View File

@ -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<SelectModuleType>();
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<SelectModuleType>();
const [createUpdateModule, setCreateUpdateModule] = useState(false);
const { user } = useUserAuth();
@ -80,44 +63,8 @@ const ProjectModules: NextPage = () => {
}, [createUpdateModule]);
return (
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${truncateText(activeProject?.name ?? "Project", 32)} Modules`} />
</Breadcrumbs>
}
right={
<div className="flex items-center gap-2">
{moduleViewOptions.map((option) => (
<Tooltip
key={option.type}
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} Layout</span>}
position="bottom"
>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
modulesView === option.type ? "bg-custom-sidebar-background-80" : "text-custom-sidebar-text-200"
}`}
onClick={() => setModulesView(option.type)}
>
<option.icon className="h-4 w-4" />
</button>
</Tooltip>
))}
<Button
variant="primary"
prependIcon={<Plus />}
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "m" });
document.dispatchEvent(e);
}}
>
Add Module
</Button>
</div>
}
<AppLayout
header={<ModulesHeader name={activeProject?.name} modulesView={modulesView} setModulesView={setModulesView} />}
>
<CreateUpdateModuleModal
isOpen={createUpdateModule}
@ -173,7 +120,7 @@ const ProjectModules: NextPage = () => {
<Loader.Item height="100px" />
</Loader>
)}
</ProjectAuthorizationWrapper>
</AppLayout>
);
};

View File

@ -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 (
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${projectDetails?.name ?? "Project"} Views`} />
</Breadcrumbs>
}
right={<ProjectViewsHeader />}
>
<AppLayout header={<ProjectViewsHeader title={projectDetails?.name} />}>
<ProjectViewsList />
</ProjectAuthorizationWrapper>
</AppLayout>
);
};

View File

@ -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;
});

View File

@ -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<IUser>;
fetchCurrentUserSettings: () => Promise<IUserSettings>;
fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise<IWorkspaceMember>;
fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise<IProjectMember>;
updateTourCompleted: () => Promise<void>;
updateCurrentUser: (data: Partial<IUser>) => Promise<IUser>;
}
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<IUser>) => {
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;

View File

@ -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;