forked from github/plane
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:
parent
e496cec49f
commit
98b1a078de
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from "./input";
|
||||
export * from "./textarea";
|
||||
export * from "./input-color-picker"
|
91
packages/ui/src/form-fields/input-color-picker.tsx
Normal file
91
packages/ui/src/form-fields/input-color-picker.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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={() => {
|
||||
|
@ -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";
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
56
web/components/headers/cycles.tsx
Normal file
56
web/components/headers/cycles.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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";
|
||||
|
87
web/components/headers/modules.tsx
Normal file
87
web/components/headers/modules.tsx
Normal 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>
|
||||
);
|
||||
};
|
31
web/components/headers/profile-preferences.tsx
Normal file
31
web/components/headers/profile-preferences.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from "./user-wrapper";
|
||||
export * from "./workspace-wrapper";
|
||||
export * from "./project-wrapper";
|
||||
|
91
web/layouts/auth-layout/project-wrapper.tsx
Normal file
91
web/layouts/auth-layout/project-wrapper.tsx
Normal 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}</>;
|
||||
};
|
@ -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 (
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
|
4
web/types/users.d.ts
vendored
4
web/types/users.d.ts
vendored
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user