Add dynamic breadcrumbs to Header via React Context
Replace hardcoded "Coffee Project / Projects" with a BreadcrumbsContext that each page registers into via useBreadcrumbs(). The Header reads breadcrumb items dynamically, renders links for items with href, and makes "Coffee Project" clickable to open the navigation drawer. Also removes the unused currentScreenName from Redux appState. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,13 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useBreadcrumbs } from "@shared/context/BreadcrumbsContext"
|
||||||
import { Button } from "@shared/ui"
|
import { Button } from "@shared/ui"
|
||||||
|
|
||||||
import cls from "./HomePage.module.scss"
|
import cls from "./HomePage.module.scss"
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
|
useBreadcrumbs([{ label: "Home" }])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cls.homepage}>
|
<div className={cls.homepage}>
|
||||||
<p className={cls.title}>Coffee Project Starter</p>
|
<p className={cls.title}>Coffee Project Starter</p>
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { JSX } from "react"
|
||||||
|
|
||||||
|
import moment from "moment"
|
||||||
|
import { FunctionComponent } from "react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
AvatarUpload,
|
||||||
|
ChangePasswordForm,
|
||||||
|
EditProfileForm,
|
||||||
|
LogoutButton,
|
||||||
|
} from "@features/profile"
|
||||||
|
import api from "@shared/api"
|
||||||
|
import { useBreadcrumbs } from "@shared/context/BreadcrumbsContext"
|
||||||
|
import { StaticLoader } from "@shared/ui/Loader"
|
||||||
|
import { Card } from "@shared/ui"
|
||||||
|
|
||||||
|
import { IProfilePageProps } from "./ProfilePage.d"
|
||||||
|
import styles from "./ProfilePage.module.scss"
|
||||||
|
|
||||||
|
export const ProfilePage: FunctionComponent<
|
||||||
|
IProfilePageProps
|
||||||
|
> = (): JSX.Element => {
|
||||||
|
useBreadcrumbs([{ label: "Profile" }])
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: user,
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
} = api.useQuery("get", "/api/users/me/")
|
||||||
|
|
||||||
|
const { mutate: updateUser } = api.useMutation(
|
||||||
|
"patch",
|
||||||
|
"/api/users/{user_id}/",
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleAvatarChange = (url: string) => {
|
||||||
|
if (!user) return
|
||||||
|
updateUser(
|
||||||
|
{
|
||||||
|
params: { path: { user_id: user.id } },
|
||||||
|
body: { avatar: url },
|
||||||
|
},
|
||||||
|
{ onSuccess: () => refetch() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) return <StaticLoader fullscreen />
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return (
|
||||||
|
<div className={styles.root} data-testid="ProfilePage">
|
||||||
|
<p>Не удалось загрузить данные пользователя</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.root} data-testid="ProfilePage">
|
||||||
|
<div className={styles.container}>
|
||||||
|
<AvatarUpload
|
||||||
|
currentAvatarUrl={user.avatar}
|
||||||
|
onAvatarChange={handleAvatarChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card className={styles.section}>
|
||||||
|
<EditProfileForm user={user} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className={styles.section}>
|
||||||
|
<ChangePasswordForm />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className={styles.section}>
|
||||||
|
<h3 className={styles.sectionTitle}>Аккаунт</h3>
|
||||||
|
<div className={styles.infoList}>
|
||||||
|
<div className={styles.infoRow}>
|
||||||
|
<span className={styles.infoLabel}>Логин</span>
|
||||||
|
<span className={styles.infoValue}>{user.username}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.infoRow}>
|
||||||
|
<span className={styles.infoLabel}>Дата регистрации</span>
|
||||||
|
<span className={styles.infoValue}>
|
||||||
|
{moment(user.date_joined).format("DD.MM.YYYY")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.infoRow}>
|
||||||
|
<span className={styles.infoLabel}>Статус</span>
|
||||||
|
<span className={styles.infoValue}>
|
||||||
|
{user.is_active ? "Активен" : "Неактивен"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<LogoutButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,13 +1,26 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import type { JSX } from "react"
|
import type { JSX } from "react"
|
||||||
|
|
||||||
|
import { useParams } from "next/navigation"
|
||||||
import { FunctionComponent } from "react"
|
import { FunctionComponent } from "react"
|
||||||
|
|
||||||
|
import { useBreadcrumbs } from "@shared/context/BreadcrumbsContext"
|
||||||
|
|
||||||
import { IProjectWorkspacePageProps } from "./ProjectWorkspacePage.d"
|
import { IProjectWorkspacePageProps } from "./ProjectWorkspacePage.d"
|
||||||
import styles from "./ProjectWorkspacePage.module.scss"
|
import styles from "./ProjectWorkspacePage.module.scss"
|
||||||
|
|
||||||
export const ProjectWorkspacePage: FunctionComponent<
|
export const ProjectWorkspacePage: FunctionComponent<
|
||||||
IProjectWorkspacePageProps
|
IProjectWorkspacePageProps
|
||||||
> = (): JSX.Element => {
|
> = (): JSX.Element => {
|
||||||
|
const params = useParams<{ project_id: string }>()
|
||||||
|
const projectId = params?.project_id ?? ""
|
||||||
|
|
||||||
|
useBreadcrumbs([
|
||||||
|
{ label: "Projects", href: "/projects" },
|
||||||
|
{ label: `Project ${projectId}` },
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root} data-testid="ProjectWorkspacePage">
|
<div className={styles.root} data-testid="ProjectWorkspacePage">
|
||||||
ProjectWorkspacePage Component
|
ProjectWorkspacePage Component
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import type { components } from "@shared/api/__generated__/openapi.types"
|
||||||
import type { JSX } from "react"
|
import type { JSX } from "react"
|
||||||
|
|
||||||
import { PlusIcon } from "lucide-react"
|
import { PlusIcon } from "lucide-react"
|
||||||
import { FunctionComponent, useState } from "react"
|
import { FunctionComponent, useState } from "react"
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
|
||||||
import { ProjectCard } from "@entities/ProjectCard"
|
import { ProjectCard } from "@entities/ProjectCard"
|
||||||
import { CreateProjectModal } from "@features/CreateProjectModal"
|
import { useBreadcrumbs } from "@shared/context/BreadcrumbsContext"
|
||||||
|
import {
|
||||||
|
CreateProjectModal,
|
||||||
|
DeleteProjectModal,
|
||||||
|
EditProjectModal,
|
||||||
|
RenameProjectModal,
|
||||||
|
} from "@features/project"
|
||||||
import api from "@shared/api"
|
import api from "@shared/api"
|
||||||
import { Button } from "@shared/ui"
|
import { Button } from "@shared/ui"
|
||||||
import { StaticLoader } from "@shared/ui/Loader"
|
import { StaticLoader } from "@shared/ui/Loader"
|
||||||
@@ -14,13 +23,18 @@ import { ProjectsHeader } from "@widgets/Projects/ProjectsHeader"
|
|||||||
|
|
||||||
import { IProjectsPageProps } from "./ProjectsPage.d"
|
import { IProjectsPageProps } from "./ProjectsPage.d"
|
||||||
import styles from "./ProjectsPage.module.scss"
|
import styles from "./ProjectsPage.module.scss"
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
|
type ProjectRead = components["schemas"]["ProjectRead"]
|
||||||
|
|
||||||
export const ProjectsPage: FunctionComponent<
|
export const ProjectsPage: FunctionComponent<
|
||||||
IProjectsPageProps
|
IProjectsPageProps
|
||||||
> = (): JSX.Element => {
|
> = (): JSX.Element => {
|
||||||
const router = useRouter();
|
useBreadcrumbs([{ label: "Projects", href: "/projects" }])
|
||||||
|
const router = useRouter()
|
||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
|
||||||
|
const [editProject, setEditProject] = useState<ProjectRead | null>(null)
|
||||||
|
const [renameProject, setRenameProject] = useState<ProjectRead | null>(null)
|
||||||
|
const [deleteProject, setDeleteProject] = useState<ProjectRead | null>(null)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: projects,
|
data: projects,
|
||||||
@@ -57,6 +71,45 @@ export const ProjectsPage: FunctionComponent<
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{editProject && (
|
||||||
|
<EditProjectModal
|
||||||
|
open
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setEditProject(null)
|
||||||
|
}}
|
||||||
|
project={editProject}
|
||||||
|
onUpdated={async () => {
|
||||||
|
await refetchProjects()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{renameProject && (
|
||||||
|
<RenameProjectModal
|
||||||
|
open
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setRenameProject(null)
|
||||||
|
}}
|
||||||
|
project={renameProject}
|
||||||
|
onRenamed={async () => {
|
||||||
|
await refetchProjects()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{deleteProject && (
|
||||||
|
<DeleteProjectModal
|
||||||
|
open
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setDeleteProject(null)
|
||||||
|
}}
|
||||||
|
project={deleteProject}
|
||||||
|
onDeleted={async () => {
|
||||||
|
await refetchProjects()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<ProjectsHeader />
|
<ProjectsHeader />
|
||||||
|
|
||||||
<div className={styles.projectList}>
|
<div className={styles.projectList}>
|
||||||
@@ -64,12 +117,14 @@ export const ProjectsPage: FunctionComponent<
|
|||||||
<ProjectCard
|
<ProjectCard
|
||||||
key={project.id}
|
key={project.id}
|
||||||
project={project}
|
project={project}
|
||||||
// Mock random progress for demo since API doesn't provide it yet
|
|
||||||
progress={project.status === "PROCESSING" ? 45 : 0}
|
progress={project.status === "PROCESSING" ? 45 : 0}
|
||||||
currentAction={
|
currentAction={
|
||||||
project.status === "PROCESSING" ? "Rendering" : undefined
|
project.status === "PROCESSING" ? "Rendering" : undefined
|
||||||
}
|
}
|
||||||
onClick={() => router.push(`/projects/${project.id}`)}
|
onClick={() => router.push(`/projects/${project.id}`)}
|
||||||
|
onEdit={() => setEditProject(project)}
|
||||||
|
onRename={() => setRenameProject(project)}
|
||||||
|
onDelete={() => setDeleteProject(project)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Provider as ReduxProvider } from "react-redux"
|
|||||||
|
|
||||||
import { store } from "@shared/store"
|
import { store } from "@shared/store"
|
||||||
|
|
||||||
|
import { BreadcrumbsProvider } from "./BreadcrumbsContext"
|
||||||
import { QueryClientProvider } from "./QueryClientProvider"
|
import { QueryClientProvider } from "./QueryClientProvider"
|
||||||
import { UserSync } from "./UserSync"
|
import { UserSync } from "./UserSync"
|
||||||
|
|
||||||
@@ -19,9 +20,11 @@ export const AppProviders = ({
|
|||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<QueryClientProvider>
|
<QueryClientProvider>
|
||||||
<UserSync />
|
<UserSync />
|
||||||
<Theme accentColor="violet" grayColor="slate" radius="medium">
|
<BreadcrumbsProvider>
|
||||||
{children}
|
<Theme accentColor="iris" grayColor="slate" radius="medium" scaling="100%">
|
||||||
</Theme>
|
{children}
|
||||||
|
</Theme>
|
||||||
|
</BreadcrumbsProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</ReduxProvider>
|
</ReduxProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
type JSX,
|
||||||
|
type ReactNode,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react"
|
||||||
|
|
||||||
|
export interface BreadcrumbItem {
|
||||||
|
label: string
|
||||||
|
href?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BreadcrumbsContextValue {
|
||||||
|
items: BreadcrumbItem[]
|
||||||
|
setItems: (items: BreadcrumbItem[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const BreadcrumbsContext = createContext<BreadcrumbsContextValue>({
|
||||||
|
items: [],
|
||||||
|
setItems: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const BreadcrumbsProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
}): JSX.Element => {
|
||||||
|
const [items, setItems] = useState<BreadcrumbItem[]>([])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BreadcrumbsContext.Provider value={{ items, setItems }}>
|
||||||
|
{children}
|
||||||
|
</BreadcrumbsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBreadcrumbs = (items: BreadcrumbItem[]): void => {
|
||||||
|
const { setItems } = useContext(BreadcrumbsContext)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setItems(items)
|
||||||
|
return () => setItems([])
|
||||||
|
}, [JSON.stringify(items)])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBreadcrumbItems = (): BreadcrumbItem[] => {
|
||||||
|
return useContext(BreadcrumbsContext).items
|
||||||
|
}
|
||||||
@@ -1,25 +1,18 @@
|
|||||||
import type { PayloadAction } from "@reduxjs/toolkit"
|
|
||||||
|
|
||||||
import { createSlice } from "@reduxjs/toolkit"
|
import { createSlice } from "@reduxjs/toolkit"
|
||||||
|
|
||||||
import { AppState } from "./types"
|
import { AppState } from "./types"
|
||||||
|
|
||||||
const initialState: AppState = {
|
const initialState: AppState = {}
|
||||||
currentScreenName: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
const appStateSlice = createSlice({
|
const appStateSlice = createSlice({
|
||||||
name: "appState",
|
name: "appState",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setCurrentScreenName(state, action: PayloadAction<string>) {
|
resetAppState() {
|
||||||
state.currentScreenName = action.payload
|
return initialState
|
||||||
},
|
|
||||||
resetAppState(state) {
|
|
||||||
state.currentScreenName = initialState.currentScreenName
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const { resetAppState, setCurrentScreenName } = appStateSlice.actions
|
export const { resetAppState } = appStateSlice.actions
|
||||||
export const appStateReducer = appStateSlice.reducer
|
export const appStateReducer = appStateSlice.reducer
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
export interface AppState {
|
export interface AppState {}
|
||||||
currentScreenName: string
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,30 +1,60 @@
|
|||||||
.root {
|
.root {
|
||||||
padding: 12px 24px;
|
padding: 0 24px;
|
||||||
background-color: variables.$color-white;
|
height: var(--header-height);
|
||||||
|
background-color: variables.$bg-default;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border-bottom: 1px solid variables.$border-default;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.screenPath {
|
.screenPath {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brandTitle {
|
.brandTitle {
|
||||||
@include typography.font-body-16(800);
|
@include typography.font-body-16(600);
|
||||||
|
color: variables.$text-primary;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: variables.$text-secondary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
@include typography.font-body-14(400);
|
||||||
|
color: variables.$text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
.separator, .screenName {
|
.screenName {
|
||||||
@include typography.font-body-14(500);
|
@include typography.font-body-14(400);
|
||||||
color: variables.$text-secondary;
|
color: variables.$text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.breadcrumbSegment {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbLink {
|
||||||
|
@include typography.font-body-14(400);
|
||||||
|
color: variables.$text-secondary;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: variables.$text-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.start {
|
.start {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,17 @@
|
|||||||
|
|
||||||
import type { JSX } from "react"
|
import type { JSX } from "react"
|
||||||
|
|
||||||
import { Menu as MenuIcon } from "lucide-react"
|
import { FolderKanban, Menu as MenuIcon } from "lucide-react"
|
||||||
|
import dynamic from "next/dynamic"
|
||||||
|
import Link from "next/link"
|
||||||
import { FunctionComponent, useState } from "react"
|
import { FunctionComponent, useState } from "react"
|
||||||
|
|
||||||
import { NavigationDrawer } from "@entities/NavigationDrawer"
|
const NavigationDrawer = dynamic(
|
||||||
|
() => import("@entities/NavigationDrawer").then((m) => m.NavigationDrawer),
|
||||||
|
{ ssr: false },
|
||||||
|
)
|
||||||
import { UserDropdown } from "@entities/UserDropdown"
|
import { UserDropdown } from "@entities/UserDropdown"
|
||||||
|
import { useBreadcrumbItems } from "@shared/context/BreadcrumbsContext"
|
||||||
import { useAppSelector } from "@shared/hooks/useAppSelector"
|
import { useAppSelector } from "@shared/hooks/useAppSelector"
|
||||||
import { Button } from "@shared/ui"
|
import { Button } from "@shared/ui"
|
||||||
|
|
||||||
@@ -15,6 +21,7 @@ import styles from "./Header.module.scss"
|
|||||||
|
|
||||||
export const Header: FunctionComponent<IHeaderProps> = (): JSX.Element => {
|
export const Header: FunctionComponent<IHeaderProps> = (): JSX.Element => {
|
||||||
const userData = useAppSelector((state) => state.user.user)
|
const userData = useAppSelector((state) => state.user.user)
|
||||||
|
const breadcrumbs = useBreadcrumbItems()
|
||||||
|
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||||
|
|
||||||
@@ -26,9 +33,24 @@ export const Header: FunctionComponent<IHeaderProps> = (): JSX.Element => {
|
|||||||
<MenuIcon size={24} />
|
<MenuIcon size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
<div className={styles.screenPath}>
|
<div className={styles.screenPath}>
|
||||||
<h1 className={styles.brandTitle}>Coffee Project</h1>
|
<h1
|
||||||
<span className={styles.separator}>/</span>
|
className={styles.brandTitle}
|
||||||
<h3 className={styles.screenName}>Projects</h3>
|
onClick={() => setIsDrawerOpen(true)}
|
||||||
|
>
|
||||||
|
Coffee Project
|
||||||
|
</h1>
|
||||||
|
{breadcrumbs.map((item, index) => (
|
||||||
|
<span key={index} className={styles.breadcrumbSegment}>
|
||||||
|
<span className={styles.separator}>/</span>
|
||||||
|
{item.href ? (
|
||||||
|
<Link href={item.href} className={styles.breadcrumbLink}>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className={styles.screenName}>{item.label}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.end}>
|
<div className={styles.end}>
|
||||||
@@ -41,6 +63,7 @@ export const Header: FunctionComponent<IHeaderProps> = (): JSX.Element => {
|
|||||||
buttons={[
|
buttons={[
|
||||||
{
|
{
|
||||||
label: "Проекты",
|
label: "Проекты",
|
||||||
|
icon: FolderKanban,
|
||||||
path: "/projects",
|
path: "/projects",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
Reference in New Issue
Block a user