fix: project layout added and theming fixes (#2455)

* fix: project layout added and theme fixes

* feat: input color picker component added to ui package

* fix: layout fixes

* fix: conflicts and build issues resolved

* fix: layout headers fixes
This commit is contained in:
sriram veeraghanta
2023-10-17 12:46:38 +05:30
committed by GitHub
parent e496cec49f
commit 98b1a078de
30 changed files with 891 additions and 699 deletions

View File

@@ -1,14 +1,9 @@
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
// cmdk
import { Command } from "cmdk";
import { THEMES_OBJ } from "constants/themes";
import { THEME_OPTIONS } from "constants/themes";
import { useTheme } from "next-themes";
import useUser from "hooks/use-user";
import { Settings } from "lucide-react";
// helper
import { unsetCustomCssVariables } from "helpers/theme.helper";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
@@ -44,7 +39,7 @@ export const ChangeInterfaceTheme: React.FC<Props> = observer(({ setIsPaletteOpe
return (
<>
{THEMES_OBJ.filter((t) => t.value !== "custom").map((theme) => (
{THEME_OPTIONS.filter((t) => t.value !== "custom").map((theme) => (
<Command.Item
key={theme.value}
onSelect={() => {

View File

@@ -18,14 +18,14 @@ import { Input } from "@plane/ui";
// icons
import { Palette } from "lucide-react";
// types
import { ICustomTheme } from "types";
import { IUserTheme } from "types";
type Props = {
name: keyof ICustomTheme;
name: keyof IUserTheme;
position?: "left" | "right";
watch: UseFormWatch<any>;
setValue: UseFormSetValue<any>;
control: Control<ICustomTheme, any>;
control: Control<IUserTheme, any>;
error: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined;
register: UseFormRegister<any>;
};
@@ -38,7 +38,7 @@ export const ColorPickerInput: FC<Props> = (props) => {
setValue(name, hex);
};
const getColorText = (colorName: keyof ICustomTheme) => {
const getColorText = (colorName: keyof IUserTheme) => {
switch (colorName) {
case "background":
return "Background";

View File

@@ -1,77 +1,54 @@
import React, { useEffect, useState } from "react";
import { FC } from "react";
import { useTheme } from "next-themes";
import { useForm } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
// ui
import { ColorPickerInput } from "components/core";
import { Button } from "@plane/ui";
import { Button, InputColorPicker } from "@plane/ui";
// types
import { ICustomTheme } from "types";
import { IUserTheme } from "types";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
type Props = {
preLoadedData?: Partial<ICustomTheme> | null;
};
type Props = {};
const defaultValues: ICustomTheme = {
background: "#0d101b",
text: "#c5c5c5",
primary: "#3f76ff",
sidebarBackground: "#0d101b",
sidebarText: "#c5c5c5",
darkPalette: false,
palette: "",
theme: "custom",
};
export const CustomThemeSelector: React.FC<Props> = observer(({ preLoadedData }) => {
const store: any = useMobxStore();
export const CustomThemeSelector: FC<Props> = observer(() => {
const { user: userStore } = useMobxStore();
const userTheme = userStore?.currentUser?.theme;
// hooks
const { setTheme } = useTheme();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [darkPalette, setDarkPalette] = useState(false);
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
control,
watch,
setValue,
reset,
} = useForm<ICustomTheme>({
defaultValues,
} = useForm<IUserTheme>({
defaultValues: {
background: userTheme?.background !== "" ? userTheme?.background : "#0d101b",
text: userTheme?.text !== "" ? userTheme?.text : "#c5c5c5",
primary: userTheme?.primary !== "" ? userTheme?.primary : "#3f76ff",
sidebarBackground: userTheme?.sidebarBackground !== "" ? userTheme?.sidebarBackground : "#0d101b",
sidebarText: userTheme?.sidebarText !== "" ? userTheme?.sidebarText : "#c5c5c5",
darkPalette: userTheme?.darkPalette || false,
palette: userTheme?.palette !== "" ? userTheme?.palette : "",
},
});
useEffect(() => {
reset({
...defaultValues,
...preLoadedData,
});
}, [preLoadedData, reset]);
const handleUpdateTheme = async (formData: any) => {
const payload: ICustomTheme = {
const payload: IUserTheme = {
background: formData.background,
text: formData.text,
primary: formData.primary,
sidebarBackground: formData.sidebarBackground,
sidebarText: formData.sidebarText,
darkPalette: darkPalette,
darkPalette: false,
palette: `${formData.background},${formData.text},${formData.primary},${formData.sidebarBackground},${formData.sidebarText}`,
theme: "custom",
};
setTheme("custom");
return store.user
.updateCurrentUserSettings({ theme: payload })
.then((response: any) => response)
.catch((error: any) => error);
return userStore.updateCurrentUser({ theme: payload });
};
return (
@@ -82,63 +59,91 @@ export const CustomThemeSelector: React.FC<Props> = observer(({ preLoadedData })
<div className="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2 md:grid-cols-3">
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Background color</h3>
<ColorPickerInput
name="background"
position="right"
<Controller
control={control}
error={errors.background}
watch={watch}
setValue={setValue}
register={register}
name="background"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="background"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.background)}
/>
)}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Text color</h3>
<ColorPickerInput
name="text"
<Controller
control={control}
error={errors.text}
watch={watch}
setValue={setValue}
register={register}
name="text"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="text"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.text)}
/>
)}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Primary(Theme) color</h3>
<ColorPickerInput
name="primary"
error={errors.primary}
<Controller
control={control}
watch={watch}
setValue={setValue}
register={register}
name="primary"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="primary"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.primary)}
/>
)}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Sidebar background color</h3>
<ColorPickerInput
name="sidebarBackground"
position="right"
<Controller
control={control}
error={errors.sidebarBackground}
watch={watch}
setValue={setValue}
register={register}
name="sidebarBackground"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="sidebarBackground"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.sidebarBackground)}
/>
)}
/>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Sidebar text color</h3>
<ColorPickerInput
name="sidebarText"
<Controller
control={control}
error={errors.sidebarText}
watch={watch}
setValue={setValue}
register={register}
name="sidebarText"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="sidebarText"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.sidebarText)}
/>
)}
/>
</div>
</div>

View File

@@ -1,130 +1,80 @@
// next-themes
import { useTheme } from "next-themes";
// hooks
import useUser from "hooks/use-user";
import { FC } from "react";
// constants
import { THEMES_OBJ } from "constants/themes";
import { THEME_OPTIONS, I_THEME_OPTION } from "constants/themes";
// ui
import { CustomSelect } from "components/ui";
// types
import { ICustomTheme } from "types";
import { unsetCustomCssVariables } from "helpers/theme.helper";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
type Props = {
setPreLoadedData: React.Dispatch<React.SetStateAction<ICustomTheme | null>>;
customThemeSelectorOptions: boolean;
setCustomThemeSelectorOptions: React.Dispatch<React.SetStateAction<boolean>>;
value: I_THEME_OPTION | null;
onChange: (value: I_THEME_OPTION) => void;
};
export const ThemeSwitch: React.FC<Props> = observer(
({ setPreLoadedData, customThemeSelectorOptions, setCustomThemeSelectorOptions }) => {
const store: any = useMobxStore();
export const ThemeSwitch: FC<Props> = (props) => {
const { value, onChange } = props;
const { user } = useUser();
const { theme, setTheme } = useTheme();
const updateUserTheme = (newTheme: string) => {
if (!user) return;
setTheme(newTheme);
return store.user
.updateCurrentUserSettings({ theme: { ...user.theme, theme: newTheme } })
.then((response: any) => response)
.catch((error: any) => error);
};
const currentThemeObj = THEMES_OBJ.find((t) => t.value === theme);
return (
<CustomSelect
value={theme}
label={
currentThemeObj ? (
<div className="flex items-center gap-2">
return (
<CustomSelect
value={value}
label={
value ? (
<div className="flex items-center gap-2">
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
style={{
borderColor: value.icon.border,
}}
>
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
className="h-full w-1/2 rounded-l-full"
style={{
borderColor: currentThemeObj.icon.border,
background: value.icon.color1,
}}
>
<div
className="h-full w-1/2 rounded-l-full"
style={{
background: currentThemeObj.icon.color1,
}}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: currentThemeObj.icon.border,
background: currentThemeObj.icon.color2,
}}
/>
</div>
{currentThemeObj.label}
</div>
) : (
"Select your theme"
)
}
onChange={({ value, type }: { value: string; type: string }) => {
if (value === "custom") {
if (user?.theme?.palette) {
setPreLoadedData({
background: user.theme?.background !== "" ? user.theme.background : "#0d101b",
text: user.theme.text !== "" ? user.theme.text : "#c5c5c5",
primary: user.theme.primary !== "" ? user.theme.primary : "#3f76ff",
sidebarBackground: user.theme.sidebarBackground !== "" ? user.theme.sidebarBackground : "#0d101b",
sidebarText: user.theme.sidebarText !== "" ? user.theme.sidebarText : "#c5c5c5",
darkPalette: false,
palette: user.theme.palette !== ",,,," ? user.theme.palette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5",
theme: "custom",
});
}
if (!customThemeSelectorOptions) setCustomThemeSelectorOptions(true);
} else {
if (customThemeSelectorOptions) setCustomThemeSelectorOptions(false);
unsetCustomCssVariables();
}
updateUserTheme(value);
document.documentElement.style.setProperty("--color-scheme", type);
}}
input
width="w-full"
>
{THEMES_OBJ.map(({ value, label, type, icon }) => (
<CustomSelect.Option key={value} value={{ value, type }}>
<div className="flex items-center gap-2">
/>
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderColor: icon.border,
borderLeftColor: value.icon.border,
background: value.icon.color2,
}}
>
<div
className="h-full w-1/2 rounded-l-full"
style={{
background: icon.color1,
}}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: icon.border,
background: icon.color2,
}}
/>
</div>
{label}
/>
</div>
</CustomSelect.Option>
))}
</CustomSelect>
);
}
);
{value.label}
</div>
) : (
"Select your theme"
)
}
onChange={onChange}
input
width="w-full"
>
{THEME_OPTIONS.map((themeOption) => (
<CustomSelect.Option key={themeOption.value} value={themeOption}>
<div className="flex items-center gap-2">
<div
className="border-1 relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border"
style={{
borderColor: themeOption.icon.border,
}}
>
<div
className="h-full w-1/2 rounded-l-full"
style={{
background: themeOption.icon.color1,
}}
/>
<div
className="h-full w-1/2 rounded-r-full border-l"
style={{
borderLeftColor: themeOption.icon.border,
background: themeOption.icon.color2,
}}
/>
</div>
{themeOption.label}
</div>
</CustomSelect.Option>
))}
</CustomSelect>
);
};

View File

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

View File

@@ -8,3 +8,6 @@ export * from "./project-views";
export * from "./workspace-analytics";
export * from "./workspace-dashboard";
export * from "./projects";
export * from "./profile-preferences";
export * from "./cycles";
export * from "./modules";

View File

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

View File

@@ -0,0 +1,31 @@
import { useRouter } from "next/router";
import { ArrowLeft } from "lucide-react";
// components
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
export const ProfilePreferencesHeader = () => {
const router = useRouter();
return (
<div
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
>
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div className="block md:hidden">
<button
type="button"
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
onClick={() => router.back()}
>
<ArrowLeft fontSize={14} strokeWidth={2} />
</button>
</div>
<div>
<Breadcrumbs>
<BreadcrumbItem title="My Profile Preferences" />
</Breadcrumbs>
</div>
</div>
</div>
);
};

View File

@@ -1,23 +1,24 @@
import { useCallback, useState } from "react";
import { useCallback, useState, FC } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { ArrowLeft, Plus } from "lucide-react";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
import { ProjectAnalyticsModal } from "components/analytics";
// ui
import { Button } from "@plane/ui";
// icons
import { Plus } from "lucide-react";
import { Breadcrumbs, BreadcrumbItem } from "components/breadcrumbs";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
// helper
import { truncateText } from "helpers/string.helper";
export const ProjectIssuesHeader: React.FC = observer(() => {
export const ProjectIssuesHeader: FC = observer(() => {
const [analyticsModal, setAnalyticsModal] = useState(false);
const router = useRouter();
@@ -100,64 +101,85 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
onClose={() => setAnalyticsModal(false)}
projectDetails={projectDetails ?? undefined}
/>
<div className="flex items-center gap-2">
<LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters">
<FilterSelection
filters={issueFilterStore.userFilters}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
<div
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
>
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div className="block md:hidden">
<button
type="button"
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
onClick={() => router.back()}
>
<ArrowLeft fontSize={14} strokeWidth={2} />
</button>
</div>
<div>
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`} />
</Breadcrumbs>
</div>
</div>
<div className="flex items-center gap-3">
<LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
</FiltersDropdown>
<FiltersDropdown title="View">
<DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
displayProperties={issueFilterStore.userDisplayProperties}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
/>
</FiltersDropdown>
{projectId && inboxStore.isInboxEnabled(projectId.toString()) && (
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxStore.getInboxId(projectId.toString())}`}>
<a>
<Button variant="neutral-primary" size="sm" className="relative">
Inbox
{inboxDetails && (
<span className="absolute -top-1.5 -right-1.5 h-4 w-4 rounded-full text-custom-text-100 bg-custom-sidebar-background-80 border border-custom-sidebar-border-200">
{inboxDetails.pending_issue_count}
</span>
)}
</Button>
</a>
</Link>
)}
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
<Button
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
}}
size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
<FiltersDropdown title="Filters">
<FilterSelection
filters={issueFilterStore.userFilters}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/>
</FiltersDropdown>
<FiltersDropdown title="View">
<DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
displayProperties={issueFilterStore.userDisplayProperties}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
/>
</FiltersDropdown>
{projectId && inboxStore.isInboxEnabled(projectId.toString()) && (
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxStore.getInboxId(projectId.toString())}`}>
<a>
<Button variant="neutral-primary" size="sm" className="relative">
Inbox
{inboxDetails && (
<span className="absolute -top-1.5 -right-1.5 h-4 w-4 rounded-full text-custom-text-100 bg-custom-sidebar-background-80 border border-custom-sidebar-border-200">
{inboxDetails.pending_issue_count}
</span>
)}
</Button>
</a>
</Link>
)}
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
<Button
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
}}
size="sm"
prependIcon={<Plus />}
>
Add Issue
</Button>
</div>
</div>
</>
);

View File

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

View File

@@ -2,12 +2,7 @@ import React from "react";
import type { Props } from "./types";
export const ModuleIcon: React.FC<Props> = ({
width = "24",
height = "24",
className,
color = "#F15B5B",
}) => (
export const ModuleIcon: React.FC<Props> = ({ width = "24", height = "24", className, color = "#F15B5B" }) => (
<svg
width={width}
height={height}
@@ -19,7 +14,7 @@ export const ModuleIcon: React.FC<Props> = ({
<path
d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z"
stroke="#F15B5B"
stroke-width="1.5"
strokeWidth="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
@@ -27,7 +22,7 @@ export const ModuleIcon: React.FC<Props> = ({
d="M5.84925 4.66667H4.81221C4.73039 4.66667 4.66406 4.733 4.66406 4.81482V5.85185C4.66406 5.93367 4.73039 6 4.81221 6H5.84925C5.93107 6 5.9974 5.93367 5.9974 5.85185V4.81482C5.9974 4.733 5.93107 4.66667 5.84925 4.66667Z"
fill={color}
stroke="#F15B5B"
stroke-width="1.5"
strokeWidth="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
@@ -35,7 +30,7 @@ export const ModuleIcon: React.FC<Props> = ({
d="M5.84925 10H4.81221C4.73039 10 4.66406 10.0663 4.66406 10.1481V11.1852C4.66406 11.267 4.73039 11.3333 4.81221 11.3333H5.84925C5.93107 11.3333 5.9974 11.267 5.9974 11.1852V10.1481C5.9974 10.0663 5.93107 10 5.84925 10Z"
fill={color}
stroke="#F15B5B"
stroke-width="1.5"
strokeWidth="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
@@ -43,7 +38,7 @@ export const ModuleIcon: React.FC<Props> = ({
d="M11.1852 4.66667H10.1481C10.0663 4.66667 10 4.733 10 4.81482V5.85185C10 5.93367 10.0663 6 10.1481 6H11.1852C11.267 6 11.3333 5.93367 11.3333 5.85185V4.81482C11.3333 4.733 11.267 4.66667 11.1852 4.66667Z"
fill={color}
stroke="#F15B5B"
stroke-width="1.5"
strokeWidth="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
@@ -51,7 +46,7 @@ export const ModuleIcon: React.FC<Props> = ({
d="M11.1852 10H10.1481C10.0663 10 10 10.0663 10 10.1481V11.1852C10 11.267 10.0663 11.3333 10.1481 11.3333H11.1852C11.267 11.3333 11.3333 11.267 11.3333 11.1852V10.1481C11.3333 10.0663 11.267 10 11.1852 10Z"
fill={color}
stroke="#F15B5B"
stroke-width="1.5"
strokeWidth="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>