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:
committed by
GitHub
parent
e496cec49f
commit
98b1a078de
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user