Добавление страницы information (#41)
This commit is contained in:
@ -95,7 +95,7 @@
|
|||||||
"@typescript-eslint/no-shadow": "warn",
|
"@typescript-eslint/no-shadow": "warn",
|
||||||
"@typescript-eslint/no-namespace": "off",
|
"@typescript-eslint/no-namespace": "off",
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
"@typescript-eslint/no-unused-vars": ["error"],
|
"@typescript-eslint/no-unused-vars": "warn",
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"array-bracket-spacing": ["warn", "never"],
|
"array-bracket-spacing": ["warn", "never"],
|
||||||
"block-spacing": ["warn", "never"],
|
"block-spacing": ["warn", "never"],
|
||||||
|
|||||||
@ -89,8 +89,8 @@ module.exports = {
|
|||||||
'^_types(.*)$': '<rootDir>/src/core/types$1',
|
'^_types(.*)$': '<rootDir>/src/core/types$1',
|
||||||
'^_utils(.*)$': '<rootDir>/src/core/utils$1',
|
'^_utils(.*)$': '<rootDir>/src/core/utils$1',
|
||||||
'^_enums(.*)$': '<rootDir>/src/core/enums$1',
|
'^_enums(.*)$': '<rootDir>/src/core/enums$1',
|
||||||
|
'^_referers(.*)$': '<rootDir>/src/core/referers$1',
|
||||||
'^_pages(.*)$': '<rootDir>/src/pages$1',
|
'^_pages(.*)$': '<rootDir>/src/pages$1',
|
||||||
'^_referers(.*)$': '<rootDir>/src/referers$1',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
import {AppBar, createStyles, Fab, IconButton, makeStyles, Theme, Toolbar} from '@material-ui/core';
|
import {AppBar, createStyles, Fab, IconButton, makeStyles, Theme, Toolbar} from '@material-ui/core';
|
||||||
import React, {memo} from 'react';
|
import React, {memo} from 'react';
|
||||||
|
import Slide from '@material-ui/core/Slide';
|
||||||
import MoreIcon from '@material-ui/icons/MoreVert';
|
import MoreIcon from '@material-ui/icons/MoreVert';
|
||||||
import AddIcon from '@material-ui/icons/Add';
|
import AddIcon from '@material-ui/icons/Add';
|
||||||
import MoveToInboxIcon from '@material-ui/icons/MoveToInbox';
|
import MoveToInboxIcon from '@material-ui/icons/MoveToInbox';
|
||||||
import CalendarTodayIcon from '@material-ui/icons/CalendarToday';
|
import CalendarTodayIcon from '@material-ui/icons/CalendarToday';
|
||||||
import ListAltIcon from '@material-ui/icons/ListAlt';
|
import ListAltIcon from '@material-ui/icons/ListAlt';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
trigger: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
iconRight: {
|
iconRight: {
|
||||||
@ -29,51 +34,52 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const BothMenu: React.FC = () => {
|
const BothMenu: React.FC<Props> = ({trigger}) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<Slide appear={false} direction="up" in={!trigger}>
|
||||||
position="fixed"
|
<AppBar
|
||||||
color="primary"
|
color="primary"
|
||||||
className={classes.appBar}
|
className={classes.appBar}
|
||||||
>
|
>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={classes.iconRight}
|
className={classes.iconRight}
|
||||||
edge="start"
|
edge="start"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
>
|
>
|
||||||
<MoveToInboxIcon />
|
<MoveToInboxIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="end"
|
edge="end"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
>
|
>
|
||||||
<ListAltIcon />
|
<ListAltIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Fab
|
<Fab
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className={classes.fabButton}
|
className={classes.fabButton}
|
||||||
>
|
>
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
</Fab>
|
</Fab>
|
||||||
<div className={classes.grow} />
|
<div className={classes.grow} />
|
||||||
<IconButton
|
<IconButton
|
||||||
className={classes.iconRight}
|
className={classes.iconRight}
|
||||||
edge="start"
|
edge="start"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
>
|
>
|
||||||
<CalendarTodayIcon />
|
<CalendarTodayIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="end"
|
edge="end"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
>
|
>
|
||||||
<MoreIcon />
|
<MoreIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
</Slide>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import React, {Fragment, memo} from 'react';
|
import React, {Fragment, memo} from 'react';
|
||||||
import {Route, Switch} from 'react-router-dom';
|
import {Route, Switch} from 'react-router-dom';
|
||||||
|
import {Container, createStyles, makeStyles, useScrollTrigger} from '@material-ui/core';
|
||||||
|
|
||||||
import mainPageRouter from '_pages/main/routing';
|
import mainPageRouter from '_pages/main/routing';
|
||||||
import chaosBoxPageRouter from '_pages/chaos-box/routing';
|
import chaosBoxPageRouter from '_pages/chaos-box/routing';
|
||||||
import calendarPageRouter from '_pages/calendar/routing';
|
import calendarPageRouter from '_pages/calendar/routing';
|
||||||
@ -13,26 +15,40 @@ import TopMenu from '../top-menu';
|
|||||||
import './Page.scss';
|
import './Page.scss';
|
||||||
import BothMenu from '../both-menu';
|
import BothMenu from '../both-menu';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() =>
|
||||||
|
createStyles({
|
||||||
|
container: {
|
||||||
|
height: '100hv',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const Page: React.FC = () => {
|
const Page: React.FC = () => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const trigger = useScrollTrigger();
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<TopMenu />
|
<div className={classes.container}>
|
||||||
<div>
|
<TopMenu trigger={trigger} />
|
||||||
<Switch>
|
<Container>
|
||||||
{mainPageRouter}
|
<Switch>
|
||||||
{chaosBoxPageRouter}
|
{mainPageRouter}
|
||||||
{calendarPageRouter}
|
{chaosBoxPageRouter}
|
||||||
{informationPageRouter}
|
{calendarPageRouter}
|
||||||
{projectsPageRouter}
|
{informationPageRouter}
|
||||||
{settingsPageRouter}
|
{projectsPageRouter}
|
||||||
{signInPageRouter}
|
{settingsPageRouter}
|
||||||
{tagsPageRouter}
|
{signInPageRouter}
|
||||||
<Route>
|
{tagsPageRouter}
|
||||||
<NotFoundPage />
|
<Route>
|
||||||
</Route>
|
<NotFoundPage />
|
||||||
</Switch>
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</Container>
|
||||||
|
<BothMenu trigger={trigger} />
|
||||||
</div>
|
</div>
|
||||||
<BothMenu />
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {useHistory} from 'react-router-dom';
|
|||||||
import {createStyles, makeStyles} from '@material-ui/core/styles';
|
import {createStyles, makeStyles} from '@material-ui/core/styles';
|
||||||
import AppBar from '@material-ui/core/AppBar';
|
import AppBar from '@material-ui/core/AppBar';
|
||||||
import Toolbar from '@material-ui/core/Toolbar';
|
import Toolbar from '@material-ui/core/Toolbar';
|
||||||
|
import Slide from '@material-ui/core/Slide';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
import IconButton from '@material-ui/core/IconButton';
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos';
|
import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos';
|
||||||
@ -13,13 +14,14 @@ import {PAGE_TITLE} from '_consts/common';
|
|||||||
import {usePageType} from '_hooks/usePageType';
|
import {usePageType} from '_hooks/usePageType';
|
||||||
import {buildPath} from '_utils/buildPath';
|
import {buildPath} from '_utils/buildPath';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
trigger: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const NO_NAME_AVATAR = 'https://d.newsweek.com/en/full/425257/02-10-putin-economy.jpg';
|
const NO_NAME_AVATAR = 'https://d.newsweek.com/en/full/425257/02-10-putin-economy.jpg';
|
||||||
|
|
||||||
const useStyles = makeStyles(() =>
|
const useStyles = makeStyles(() =>
|
||||||
createStyles({
|
createStyles({
|
||||||
root: {
|
|
||||||
flexGrow: 1,
|
|
||||||
},
|
|
||||||
title: {
|
title: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -28,7 +30,7 @@ const useStyles = makeStyles(() =>
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const TopMenu: React.FC = () => {
|
const TopMenu: React.FC<Props> = ({trigger}) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const pageType = usePageType();
|
const pageType = usePageType();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@ -39,8 +41,8 @@ const TopMenu: React.FC = () => {
|
|||||||
|
|
||||||
const title = PAGE_TITLE[pageType];
|
const title = PAGE_TITLE[pageType];
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<Slide appear={false} direction="down" in={!trigger}>
|
||||||
<AppBar position="static">
|
<AppBar position="sticky">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
{pageType === PageType.Main && (
|
{pageType === PageType.Main && (
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -70,7 +72,7 @@ const TopMenu: React.FC = () => {
|
|||||||
<Avatar src={NO_NAME_AVATAR} />
|
<Avatar src={NO_NAME_AVATAR} />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
</div>
|
</Slide>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,14 @@ import {HashRouter} from 'react-router-dom';
|
|||||||
import App from './components/page';
|
import App from './components/page';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
/*
|
||||||
<HashRouter >
|
* Выключаем стрикт мод, пока не починят
|
||||||
<App />
|
* https://github.com/mui-org/material-ui/issues/13394
|
||||||
</HashRouter>
|
*/
|
||||||
</React.StrictMode>,
|
// <React.StrictMode>
|
||||||
|
<HashRouter >
|
||||||
|
<App />
|
||||||
|
</HashRouter>,
|
||||||
|
// </React.StrictMode>
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import {makeApi} from '_utils/makeApi';
|
||||||
import {http} from '../infrastructure/Http';
|
import {http} from '../infrastructure/Http';
|
||||||
import {makeApi} from '../utils/makeApi';
|
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {PageType} from '../enums/common';
|
import {PageType} from '_enums/common';
|
||||||
|
|
||||||
export const ROUTES = {
|
export const ROUTES = {
|
||||||
MAIN: '/',
|
MAIN: '/',
|
||||||
|
|||||||
@ -19,3 +19,9 @@ export enum PageType {
|
|||||||
Settings = 'settings',
|
Settings = 'settings',
|
||||||
SigIn = 'sign-in',
|
SigIn = 'sign-in',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Icon {
|
||||||
|
AcUnit = 'acUnit',
|
||||||
|
Apple = 'apple',
|
||||||
|
Apartment = 'apartment',
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import {useMemo} from 'react';
|
import {useMemo} from 'react';
|
||||||
import {useLocation} from 'react-router-dom';
|
import {useLocation} from 'react-router-dom';
|
||||||
import {getPageType} from '../utils/common';
|
import {getPageType} from '_utils/common';
|
||||||
|
|
||||||
export const usePageType = () => {
|
export const usePageType = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import {useMemo} from 'react';
|
import {useMemo} from 'react';
|
||||||
import {useParams as useReactParams} from 'react-router-dom';
|
import {useParams as useReactParams} from 'react-router-dom';
|
||||||
import {getParamsFromUrl} from '../utils/getParamFromUrl';
|
import {getParamsFromUrl} from '_utils/getParamFromUrl';
|
||||||
import {QueryParsers} from '../utils/getQueryFromUrl';
|
import {QueryParsers} from '_utils/getQueryFromUrl';
|
||||||
|
|
||||||
export function useParams<T extends Record<string, unknown>>(paramParsers: QueryParsers<T>) {
|
export function useParams<T extends Record<string, unknown>>(paramParsers: QueryParsers<T>) {
|
||||||
const params = useReactParams<Record<keyof T, string>>();
|
const params = useReactParams<Record<keyof T, string>>();
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import {useMemo} from 'react';
|
import {useMemo} from 'react';
|
||||||
import {useLocation} from 'react-router-dom';
|
import {useLocation} from 'react-router-dom';
|
||||||
import {getQueryFromUrl, QueryParsers} from '../utils/getQueryFromUrl';
|
import {getQueryFromUrl, QueryParsers} from '_utils/getQueryFromUrl';
|
||||||
|
|
||||||
export function useQuery<T extends Record<string, unknown>>(queryParsers: QueryParsers<T>): T {
|
export function useQuery<T extends Record<string, unknown>>(queryParsers: QueryParsers<T>): T {
|
||||||
const {search} = useLocation();
|
const {search} = useLocation();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import {v4} from 'uuid';
|
import {v4} from 'uuid';
|
||||||
import {TaskStatus} from '../enums/common';
|
import {TaskStatus} from '_enums/common';
|
||||||
import {Task} from '../types/common';
|
import {Task} from '_types/common';
|
||||||
import {createService} from '../utils/createService';
|
import {createService} from '_utils/createService';
|
||||||
import {makeLocalStorageService} from './LocalStorageService';
|
import {makeLocalStorageService} from './LocalStorageService';
|
||||||
|
|
||||||
const TASK_STORAGE_NAME = 'FYB_TASK_STORAGE';
|
const TASK_STORAGE_NAME = 'FYB_TASK_STORAGE';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {FolderType, TaskStatus} from '../enums/common';
|
import {FolderType, Icon, TaskStatus} from '_enums/common';
|
||||||
|
|
||||||
export type Task = {
|
export type Task = {
|
||||||
/**
|
/**
|
||||||
@ -10,6 +10,7 @@ export type Task = {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
start_at?: string;
|
start_at?: string;
|
||||||
end_at?: string;
|
end_at?: string;
|
||||||
|
icon?: Icon;
|
||||||
/**
|
/**
|
||||||
* Контекст выполнения, теги
|
* Контекст выполнения, теги
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {PageType} from '../../enums/common';
|
import {PageType} from '_enums/common';
|
||||||
import {buildPath, BuildPathOptions} from '../buildPath';
|
import {buildPath, BuildPathOptions} from '../buildPath';
|
||||||
|
|
||||||
describe('buildPath', () => {
|
describe('buildPath', () => {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {PageType} from '../../enums/common';
|
import {PageType} from '_enums/common';
|
||||||
import {getParamsFromUrl} from '../getParamFromUrl';
|
import {getParamsFromUrl} from '../getParamFromUrl';
|
||||||
import {QueryParsers} from '../getQueryFromUrl';
|
import {QueryParsers} from '../getQueryFromUrl';
|
||||||
import {booleanParser, numberParser, stringParser} from '../queryParsers';
|
import {booleanParser, numberParser, stringParser} from '../queryParsers';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {PageType} from '../../enums/common';
|
import {PageType} from '_enums/common';
|
||||||
import {getQueryFromUrl, QueryParsers} from '../getQueryFromUrl';
|
import {getQueryFromUrl, QueryParsers} from '../getQueryFromUrl';
|
||||||
import {arrayParser, booleanParser, numberParser, stringParser} from '../queryParsers';
|
import {arrayParser, booleanParser, numberParser, stringParser} from '../queryParsers';
|
||||||
|
|
||||||
|
|||||||
49
src/core/utils/__test__/makeTreeList.test.ts
Normal file
49
src/core/utils/__test__/makeTreeList.test.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import {Folder, Task} from '_types/common';
|
||||||
|
import {FolderType, TaskStatus} from '_enums/common';
|
||||||
|
import {makeMap, makeTreeList, ROOT_TREE} from '../makeTreeList';
|
||||||
|
|
||||||
|
const Folder_1 = {id: '1', name: '1', type: FolderType.Information};
|
||||||
|
const Folder_2 = {id: '2', name: '2', type: FolderType.Information, folder: '999'};
|
||||||
|
const Folder_3 = {id: '3', name: '3', type: FolderType.Information};
|
||||||
|
const Folder_4 = {id: '4', name: '4', type: FolderType.Information, folder: '1'};
|
||||||
|
|
||||||
|
const Task_1 = {id: '11', created_at: '', status: TaskStatus.Progress};
|
||||||
|
const Task_2 = {id: '22', created_at: '', status: TaskStatus.Progress, folder: '1'};
|
||||||
|
const Task_3 = {id: '33', created_at: '', status: TaskStatus.Progress, folder: '4'};
|
||||||
|
const Task_4 = {id: '44', created_at: '', status: TaskStatus.Progress, folder: '999'};
|
||||||
|
|
||||||
|
const folders: Folder[] = [Folder_1, Folder_2, Folder_3, Folder_4];
|
||||||
|
const tasks: Task[] = [Task_1, Task_2, Task_3, Task_4];
|
||||||
|
|
||||||
|
describe('makeTreeList', () => {
|
||||||
|
it('Создает хеш-таблицу', () => {
|
||||||
|
expect(makeMap(folders, tasks)).toEqual({
|
||||||
|
[ROOT_TREE]: [Folder_1, Folder_2, Folder_3, Task_1, Task_4],
|
||||||
|
'1': [Folder_4, Task_2],
|
||||||
|
'2': [],
|
||||||
|
'3': [],
|
||||||
|
'4': [Task_3],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Создает дерево', () => {
|
||||||
|
expect(makeTreeList(folders, tasks)).toEqual([
|
||||||
|
{
|
||||||
|
data: Folder_1,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
data: Folder_4,
|
||||||
|
children: [
|
||||||
|
{data: Task_3},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{data: Task_2},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{data: Folder_2},
|
||||||
|
{data: Folder_3},
|
||||||
|
{data: Task_1},
|
||||||
|
{data: Task_4},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import {decode, ParsedUrlQueryInput, stringify} from 'querystring';
|
import {decode, ParsedUrlQueryInput, stringify} from 'querystring';
|
||||||
import {PageType} from '../enums/common';
|
import {PageType} from '_enums/common';
|
||||||
|
|
||||||
export type BuildPathOptions = {
|
export type BuildPathOptions = {
|
||||||
pageType: PageType;
|
pageType: PageType;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {PageType} from '../enums/common';
|
import {PageType} from '_enums/common';
|
||||||
import {isPageType} from '../referers/common';
|
import {isPageType} from '_referers/common';
|
||||||
|
|
||||||
export const numberToString = (num: number): string => num.toString();
|
export const numberToString = (num: number): string => num.toString();
|
||||||
|
|
||||||
|
|||||||
78
src/core/utils/makeTreeList.ts
Normal file
78
src/core/utils/makeTreeList.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import {sortBy} from 'lodash';
|
||||||
|
import {isNotEmpty} from '_referers/common';
|
||||||
|
import {Folder, Task} from '_types/common';
|
||||||
|
|
||||||
|
export type FolderItem = {
|
||||||
|
data: Folder;
|
||||||
|
children?: TreeList;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TaskItem = {
|
||||||
|
data: Task;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TreeList = Array<FolderItem | TaskItem>;
|
||||||
|
|
||||||
|
export type FolderTaskMap = Record<string, Array<(Folder | Task)> | undefined>;
|
||||||
|
|
||||||
|
export const isFolderItem = (item: FolderItem | TaskItem): item is FolderItem => (
|
||||||
|
'type' in item.data
|
||||||
|
);
|
||||||
|
export const isTaskItem = (item: FolderItem | TaskItem): item is TaskItem => (
|
||||||
|
'status' in item.data
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ROOT_TREE = '__root__';
|
||||||
|
|
||||||
|
export const makeMap = (folders: Folder[], tasks: Task[]) => {
|
||||||
|
const INIT_MAP: FolderTaskMap = {
|
||||||
|
[ROOT_TREE]: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Собираем хеш-таблицу существующих папок с предзаполненным root элементом
|
||||||
|
*/
|
||||||
|
const map = folders.reduce<FolderTaskMap>((acc, folder) => ({
|
||||||
|
...acc,
|
||||||
|
[folder.id]: [],
|
||||||
|
}), INIT_MAP);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Наполняем хеш-таблицу мутабельно для скорости. Если у папки или таски folder.id,
|
||||||
|
* которого уже не существует, то кладем их в root
|
||||||
|
*/
|
||||||
|
folders.forEach(folder => {
|
||||||
|
const parent = folder.folder ?? ROOT_TREE;
|
||||||
|
(map[parent] ?? map[ROOT_TREE])?.push(folder);
|
||||||
|
});
|
||||||
|
|
||||||
|
tasks.forEach(task => {
|
||||||
|
const parent = task.folder ?? ROOT_TREE;
|
||||||
|
(map[parent] ?? map[ROOT_TREE])?.push(task);
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeTree = (parentId: string, map: FolderTaskMap): TreeList | undefined => {
|
||||||
|
const list = map[parentId];
|
||||||
|
|
||||||
|
if (isNotEmpty(list)) {
|
||||||
|
return sortBy(list, ['type']).map(item => {
|
||||||
|
if ('type' in item) {
|
||||||
|
const children = makeTree(item.id, map);
|
||||||
|
return {
|
||||||
|
...(children ? {children} : {}),
|
||||||
|
data: item,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: item,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeTreeList = (folders: Folder[], tasks: Task[]) => {
|
||||||
|
return makeTree(ROOT_TREE, makeMap(folders, tasks));
|
||||||
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import ru from 'convert-layout/ru';
|
import ru from 'convert-layout/ru';
|
||||||
import {isEmpty, isNotEmpty} from '../referers/common';
|
import {isEmpty, isNotEmpty} from '_referers/common';
|
||||||
|
|
||||||
export function performTextSearch<T, K extends keyof T>(items: T[], searchText: string, searchProperties: K[]) {
|
export function performTextSearch<T, K extends keyof T>(items: T[], searchText: string, searchProperties: K[]) {
|
||||||
if (isEmpty(items) || isEmpty(searchText)) {
|
if (isEmpty(items) || isEmpty(searchText)) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {isNotEmpty} from '../referers/common';
|
import {isNotEmpty} from '_referers/common';
|
||||||
|
|
||||||
export function toRequestParamValue<T>(val: T): T;
|
export function toRequestParamValue<T>(val: T): T;
|
||||||
export function toRequestParamValue<T>(val?: T): Undefinable<T>;
|
export function toRequestParamValue<T>(val?: T): Undefinable<T>;
|
||||||
|
|||||||
@ -0,0 +1,53 @@
|
|||||||
|
import {Collapse, ListItemIcon, ListItem, ListItemText} from '@material-ui/core';
|
||||||
|
import ExpandLess from '@material-ui/icons/ExpandLess';
|
||||||
|
import ExpandMore from '@material-ui/icons/ExpandMore';
|
||||||
|
import FolderIcon from '@material-ui/icons/Folder';
|
||||||
|
import React, {FC, Fragment, memo, useCallback, useMemo} from 'react';
|
||||||
|
import {isNotEmpty} from '_referers/common';
|
||||||
|
import {FolderItem} from '_utils/makeTreeList';
|
||||||
|
import InfoList from './InfoList';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
item: FolderItem;
|
||||||
|
className: string;
|
||||||
|
space: number;
|
||||||
|
openFolderIds: string[];
|
||||||
|
onFolderClick: (folderId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FolderListItem: FC<Props> = ({item, className, space, openFolderIds, onFolderClick}) => {
|
||||||
|
const isOpen = useMemo(() => {
|
||||||
|
return openFolderIds.some(id => id === item.data.id);
|
||||||
|
}, [openFolderIds, item.data.id]);
|
||||||
|
|
||||||
|
const isNotEmptyChildren = useMemo(() => isNotEmpty(item.children), [item.children]);
|
||||||
|
|
||||||
|
const handleItemClick = useCallback(() => {
|
||||||
|
onFolderClick(item.data.id);
|
||||||
|
}, [onFolderClick, item.data.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<ListItem
|
||||||
|
className={className}
|
||||||
|
button
|
||||||
|
onClick={handleItemClick}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<FolderIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={item.data.name} />
|
||||||
|
{isNotEmptyChildren && (
|
||||||
|
isOpen ? <ExpandLess /> : <ExpandMore />
|
||||||
|
)}
|
||||||
|
</ListItem>
|
||||||
|
{isNotEmptyChildren && (
|
||||||
|
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
||||||
|
<InfoList list={item.children} space={space + 1} />
|
||||||
|
</Collapse>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(FolderListItem);
|
||||||
60
src/pages/information/components/info-list/InfoList.tsx
Normal file
60
src/pages/information/components/info-list/InfoList.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import {createStyles, makeStyles, Theme, List} from '@material-ui/core';
|
||||||
|
import {xor} from 'lodash';
|
||||||
|
import React, {Fragment, memo, useCallback, useState} from 'react';
|
||||||
|
import {isFolderItem, isTaskItem, TreeList} from '_utils/makeTreeList';
|
||||||
|
import FolderListItem from './FolderListItem';
|
||||||
|
import TaskListItem from './TaskListItem';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
list: Undefinable<TreeList>;
|
||||||
|
space: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyles = makeStyles<Theme, {space: number}>((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
root: {
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 360,
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
nested: ({space}) => ({
|
||||||
|
paddingLeft: theme.spacing(4 * space),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const InfoList: React.FC<Props> = ({list, space}) => {
|
||||||
|
const [openFolderIds, setOpenFolder] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const handleSetOpenFolder = useCallback((id: string) => {
|
||||||
|
setOpenFolder(xor(openFolderIds, [id]));
|
||||||
|
}, [setOpenFolder, openFolderIds]);
|
||||||
|
|
||||||
|
const classes = useStyles({space});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List component="div" className={classes.root}>
|
||||||
|
{list?.map(item => (
|
||||||
|
<Fragment key={item.data.id}>
|
||||||
|
{isFolderItem(item) && (
|
||||||
|
<FolderListItem
|
||||||
|
item={item}
|
||||||
|
className={classes.nested}
|
||||||
|
space={space}
|
||||||
|
openFolderIds={openFolderIds}
|
||||||
|
onFolderClick={handleSetOpenFolder}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isTaskItem(item) && (
|
||||||
|
<TaskListItem
|
||||||
|
item={item}
|
||||||
|
className={classes.nested}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(InfoList);
|
||||||
22
src/pages/information/components/info-list/TaskListItem.tsx
Normal file
22
src/pages/information/components/info-list/TaskListItem.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {ListItemIcon, ListItem, ListItemText} from '@material-ui/core';
|
||||||
|
import AssignmentLateIcon from '@material-ui/icons/AssignmentLate';
|
||||||
|
import React, {FC, memo} from 'react';
|
||||||
|
import {TaskItem} from '_utils/makeTreeList';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
item: TaskItem;
|
||||||
|
className: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TaskListItem: FC<Props> = ({item, className}) => {
|
||||||
|
return (
|
||||||
|
<ListItem button className={className}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<AssignmentLateIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={item.data.title} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(TaskListItem);
|
||||||
1
src/pages/information/components/info-list/index.ts
Normal file
1
src/pages/information/components/info-list/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export {default} from './InfoList';
|
||||||
@ -1,8 +1,18 @@
|
|||||||
import React, {memo} from 'react';
|
import React, {Fragment, memo} from 'react';
|
||||||
|
import {isNotEmpty} from '_referers/common';
|
||||||
|
import {makeTreeList} from '_utils/makeTreeList';
|
||||||
|
import {FolderList, TaskList} from '../../consts';
|
||||||
|
import InfoList from '../info-list';
|
||||||
|
|
||||||
|
const tree = makeTreeList(FolderList, TaskList);
|
||||||
|
|
||||||
const Page: React.FC = () => {
|
const Page: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div>information</div>
|
<Fragment>
|
||||||
|
{isNotEmpty(tree) && (
|
||||||
|
<InfoList list={tree} space={1} />
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
32
src/pages/information/consts.ts
Normal file
32
src/pages/information/consts.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {v4} from 'uuid';
|
||||||
|
import {FolderType, Icon, TaskStatus} from '_enums/common';
|
||||||
|
import {Folder, Tag, Task} from '_types/common';
|
||||||
|
|
||||||
|
// Псевдоданные
|
||||||
|
export const TagList: Tag[] = [
|
||||||
|
{id: '33', name: 'Tag', color: '#2fc036'},
|
||||||
|
{id: '66', name: 'Tag', color: '#2fc036'},
|
||||||
|
{id: '77', name: 'Tag', color: '#2fc036'},
|
||||||
|
{id: '22', name: 'Tag', color: '#2fc036'},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FolderList: Folder[] = [
|
||||||
|
{id: '4', name: 'Folder 1', type: FolderType.Information},
|
||||||
|
{id: '6', name: 'Folder 34', type: FolderType.Information},
|
||||||
|
{id: '7', name: 'Folder ffd', type: FolderType.Information},
|
||||||
|
{id: '73', name: 'Folder ffd', type: FolderType.Information, folder: '7'},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TaskList: Task[] = [
|
||||||
|
{id: v4(), title: 'Title number 1', body: 'Description', created_at: '2019-01-01T13:00', icon: Icon.Apple, status: TaskStatus.Progress, folder: '4'},
|
||||||
|
{id: v4(), title: 'Title number 2', body: 'Description', created_at: '2019-01-01T13:00', icon: Icon.Apple, status: TaskStatus.Progress, folder: '4'},
|
||||||
|
{id: v4(), title: 'Title number 3', body: 'Description', created_at: '2019-01-01T13:00', icon: Icon.Apple, status: TaskStatus.Progress, folder: '6'},
|
||||||
|
{id: v4(), title: 'Title number 4', body: 'Description', created_at: '2019-01-01T13:00', icon: Icon.Apple, status: TaskStatus.Progress, tags: ['33', '77']},
|
||||||
|
{id: v4(), title: 'Title number 5', body: 'Description', created_at: '2019-01-01T13:00', icon: Icon.Apple, status: TaskStatus.Progress},
|
||||||
|
{id: v4(), title: 'Title number 6', body: 'Description', created_at: '2019-01-01T13:00', icon: Icon.Apple, status: TaskStatus.Progress},
|
||||||
|
{id: v4(), title: 'Title number 7', body: 'Description', created_at: '2019-01-01T13:00', icon: Icon.Apple, status: TaskStatus.Progress, folder: '4'},
|
||||||
|
{id: v4(), title: 'Title number 8', body: 'Description', created_at: '2019-01-01T13:00', icon: Icon.Apple, status: TaskStatus.Progress, folder: '4'},
|
||||||
|
{id: v4(), title: 'Title number 9', body: 'Description', created_at: '2019-01-01T13:00', icon: Icon.Apple, status: TaskStatus.Progress, folder: '7'},
|
||||||
|
{id: v4(), title: 'Title number 10', body: 'Description', created_at: '2019-01-01T13:00', icon: Icon.Apple, status: TaskStatus.Progress, folder: '7'},
|
||||||
|
{id: v4(), title: 'Title number 11', body: 'Description', created_at: '2019-01-01T13:00', icon: Icon.Apple, status: TaskStatus.Progress, folder: '73'},
|
||||||
|
];
|
||||||
@ -29,8 +29,8 @@
|
|||||||
"_types/*": ["./src/core/types/*"],
|
"_types/*": ["./src/core/types/*"],
|
||||||
"_utils/*": ["./src/core/utils/*"],
|
"_utils/*": ["./src/core/utils/*"],
|
||||||
"_enums/*": ["./src/core/enums/*"],
|
"_enums/*": ["./src/core/enums/*"],
|
||||||
|
"_referers/*": ["./src/core/referers/*"],
|
||||||
"_pages/*": ["./src/pages/*"],
|
"_pages/*": ["./src/pages/*"],
|
||||||
"_referers/*": ["./src/referers/*"],
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||
@ -38,8 +38,8 @@ module.exports = {
|
|||||||
_services: path.resolve(__dirname, 'src/core/services/'),
|
_services: path.resolve(__dirname, 'src/core/services/'),
|
||||||
_utils: path.resolve(__dirname, 'src/core/utils/'),
|
_utils: path.resolve(__dirname, 'src/core/utils/'),
|
||||||
_enums: path.resolve(__dirname, 'src/core/enums/'),
|
_enums: path.resolve(__dirname, 'src/core/enums/'),
|
||||||
|
_referers: path.resolve(__dirname, 'src/core/referers/'),
|
||||||
_pages: path.resolve(__dirname, 'src/pages/'),
|
_pages: path.resolve(__dirname, 'src/pages/'),
|
||||||
_referers: path.resolve(__dirname, 'src/referers/'),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
|
|||||||
Reference in New Issue
Block a user