Compare commits
10 Commits
7a114c1752
...
aa82b52fbe
| Author | SHA1 | Date | |
|---|---|---|---|
| aa82b52fbe | |||
| 9350f540af | |||
| 9d6ffd6c4a | |||
| 38d70b3f39 | |||
| 01087b7f79 | |||
| c0b21882ce | |||
| 4f389c47f5 | |||
| 5078a2cf4b | |||
| 555f68ebc6 | |||
| 72988d7d34 |
@ -47,7 +47,7 @@
|
|||||||
"import/extensions": 0,
|
"import/extensions": 0,
|
||||||
"react/prop-types": 0,
|
"react/prop-types": 0,
|
||||||
"no-underscore-dangle": 0,
|
"no-underscore-dangle": 0,
|
||||||
"import/imports-first": ["error", "absolute-first"],
|
"import/imports-first": ["warn", "absolute-first"],
|
||||||
"import/prefer-default-export": 0,
|
"import/prefer-default-export": 0,
|
||||||
"import/no-unresolved": 0,
|
"import/no-unresolved": 0,
|
||||||
"import/newline-after-import": "error",
|
"import/newline-after-import": "error",
|
||||||
@ -97,6 +97,7 @@
|
|||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "warn",
|
"@typescript-eslint/no-unused-vars": "warn",
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/ban-types": "off",
|
||||||
"array-bracket-spacing": ["warn", "never"],
|
"array-bracket-spacing": ["warn", "never"],
|
||||||
"block-spacing": ["warn", "never"],
|
"block-spacing": ["warn", "never"],
|
||||||
"brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
|
"brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
|
||||||
|
|||||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* @vigdorov @mikhailKilin
|
||||||
8139
package-lock.json
generated
8139
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,10 +8,12 @@
|
|||||||
"@material-ui/icons": "^4.9.1",
|
"@material-ui/icons": "^4.9.1",
|
||||||
"@most/adapter": "^1.0.0",
|
"@most/adapter": "^1.0.0",
|
||||||
"@most/core": "^1.6.1",
|
"@most/core": "^1.6.1",
|
||||||
|
"@most/hold": "^4.1.0",
|
||||||
"@most/scheduler": "^1.3.0",
|
"@most/scheduler": "^1.3.0",
|
||||||
"axios": "^0.21.0",
|
"axios": "^0.21.0",
|
||||||
"convert-layout": "^0.8.2",
|
"convert-layout": "^0.8.2",
|
||||||
"date-fns": "^2.16.1",
|
"date-fns": "^2.16.1",
|
||||||
|
"formik": "^2.2.6",
|
||||||
"fp-ts": "^2.8.5",
|
"fp-ts": "^2.8.5",
|
||||||
"history": "^5.0.0",
|
"history": "^5.0.0",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
|
|||||||
@ -10,19 +10,19 @@ import {NavLink} from 'react-router-dom';
|
|||||||
import {ROUTES} from '_consts/common';
|
import {ROUTES} from '_consts/common';
|
||||||
import ToggleMenu from '../toggle-menu';
|
import ToggleMenu from '../toggle-menu';
|
||||||
import {BOTH_MENU_LINKS} from '../../consts';
|
import {BOTH_MENU_LINKS} from '../../consts';
|
||||||
import PopupList from '../popup-list/PopupList';
|
import PopupList from '../popup-list';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
iconRight: {
|
iconRight: {
|
||||||
marginRight: theme.spacing(2),
|
marginRight: theme.spacing(2)
|
||||||
},
|
},
|
||||||
appBar: {
|
appBar: {
|
||||||
top: 'auto',
|
top: 'auto',
|
||||||
bottom: 0,
|
bottom: 0
|
||||||
},
|
},
|
||||||
grow: {
|
grow: {
|
||||||
flexGrow: 1,
|
flexGrow: 1
|
||||||
},
|
},
|
||||||
fabButton: {
|
fabButton: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@ -30,9 +30,9 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
top: -30,
|
top: -30,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
margin: '0 auto',
|
margin: '0 auto'
|
||||||
},
|
}
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -44,58 +44,37 @@ const BothMenu: React.FC<Props> = ({trigger}) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Slide appear={false} direction="up" in={!trigger}>
|
<Slide appear={false} direction="up" in={!trigger}>
|
||||||
<AppBar
|
<AppBar position="fixed" color="primary" className={classes.appBar}>
|
||||||
position="fixed"
|
|
||||||
color="primary"
|
|
||||||
className={classes.appBar}
|
|
||||||
>
|
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton
|
<IconButton className={classes.iconRight} edge="start" color="inherit">
|
||||||
className={classes.iconRight}
|
|
||||||
edge="start"
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
<NavLink to={ROUTES.CHAOS_BOX}>
|
<NavLink to={ROUTES.CHAOS_BOX}>
|
||||||
<MoveToInboxIcon />
|
<MoveToInboxIcon />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton edge="end" color="inherit">
|
||||||
edge="end"
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
<NavLink to={ROUTES.PROJECTS}>
|
<NavLink to={ROUTES.PROJECTS}>
|
||||||
<ListAltIcon />
|
<ListAltIcon />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<PopupList>
|
<PopupList>
|
||||||
<Fab
|
<Fab color="secondary" className={classes.fabButton}>
|
||||||
color="secondary"
|
|
||||||
className={classes.fabButton}
|
|
||||||
>
|
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
</Fab>
|
</Fab>
|
||||||
</PopupList>
|
</PopupList>
|
||||||
<div className={classes.grow} />
|
<div className={classes.grow} />
|
||||||
<IconButton
|
<IconButton className={classes.iconRight} edge="start" color="inherit">
|
||||||
className={classes.iconRight}
|
|
||||||
edge="start"
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
<NavLink to={ROUTES.CALENDAR}>
|
<NavLink to={ROUTES.CALENDAR}>
|
||||||
<CalendarTodayIcon />
|
<CalendarTodayIcon />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<ToggleMenu items={BOTH_MENU_LINKS}>
|
<ToggleMenu items={BOTH_MENU_LINKS}>
|
||||||
<IconButton
|
<IconButton edge="end" color="inherit">
|
||||||
edge="end"
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
<MoreIcon />
|
<MoreIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ToggleMenu>
|
</ToggleMenu>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
</Slide >
|
</Slide>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
126
src/app/components/create-task-modal/CreateTaskModal.tsx
Normal file
126
src/app/components/create-task-modal/CreateTaskModal.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import React, {FC, memo, useCallback} from 'react';
|
||||||
|
import {useHistory} from 'react-router-dom';
|
||||||
|
import {format} from 'date-fns';
|
||||||
|
import {useFormik} from 'formik';
|
||||||
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
|
import DialogActions from '@material-ui/core/DialogActions';
|
||||||
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
|
import {Task} from '_types/common';
|
||||||
|
import {VIEW_DATE_TIME} from '_consts/common';
|
||||||
|
import {buildPath} from '_utils/buildPath';
|
||||||
|
import {Icon, PageType} from '_enums/common';
|
||||||
|
import {Button, TextField, MenuItem} from '@material-ui/core';
|
||||||
|
import AccessAlarmIcon from '@material-ui/icons/AccessAlarm';
|
||||||
|
import AddIcCallIcon from '@material-ui/icons/AddIcCall';
|
||||||
|
import AirplanemodeActiveIcon from '@material-ui/icons/AirplanemodeActive';
|
||||||
|
import {LABELS} from '../../consts';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const now = format(new Date(), VIEW_DATE_TIME);
|
||||||
|
|
||||||
|
const CreateTaskModal: FC<Props> = ({isOpen}) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const form = useFormik<Partial<Task>>({
|
||||||
|
initialValues: {
|
||||||
|
title: '',
|
||||||
|
body: '',
|
||||||
|
start_at: now,
|
||||||
|
end_at: '',
|
||||||
|
icon: Icon.AcUnit
|
||||||
|
},
|
||||||
|
onSubmit: () => {
|
||||||
|
// В аргументах приходят values. Ждем задачи со сторами для формы
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
history.push(buildPath({pageType: PageType.Main}));
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen}>
|
||||||
|
<form onSubmit={form.handleSubmit}>
|
||||||
|
<DialogTitle id="form-dialog-title">{LABELS.CREATE_TASK}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
value={form.values.title}
|
||||||
|
id="title"
|
||||||
|
onChange={form.handleChange}
|
||||||
|
margin="dense"
|
||||||
|
label={LABELS.TITLE}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
value={form.values.body}
|
||||||
|
id="body"
|
||||||
|
onChange={form.handleChange}
|
||||||
|
margin="dense"
|
||||||
|
label={LABELS.DESCRIPTION}
|
||||||
|
rowsMax={4}
|
||||||
|
multiline
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
value={form.values.icon}
|
||||||
|
id="icon"
|
||||||
|
name="icon"
|
||||||
|
onChange={form.handleChange}
|
||||||
|
margin="dense"
|
||||||
|
label={LABELS.ADD_ICON}
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<MenuItem value={Icon.AcUnit}>
|
||||||
|
<AccessAlarmIcon fontSize="default" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={Icon.Apple}>
|
||||||
|
<AddIcCallIcon fontSize="default" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={Icon.Apartment}>
|
||||||
|
<AirplanemodeActiveIcon fontSize="default" />
|
||||||
|
</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
id="start_at"
|
||||||
|
value={form.values.start_at}
|
||||||
|
label={LABELS.START_AT}
|
||||||
|
type="datetime-local"
|
||||||
|
onChange={form.handleChange}
|
||||||
|
margin="dense"
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="end_at"
|
||||||
|
value={form.values.end_at}
|
||||||
|
label={LABELS.END_AT}
|
||||||
|
type="datetime-local"
|
||||||
|
onChange={form.handleChange}
|
||||||
|
margin="dense"
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClose} color="primary" type="button">
|
||||||
|
{LABELS.CANCEL}
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" type="submit">
|
||||||
|
{LABELS.CREATE}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(CreateTaskModal);
|
||||||
1
src/app/components/create-task-modal/index.ts
Normal file
1
src/app/components/create-task-modal/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export {default} from './CreateTaskModal';
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import React, {Fragment, memo} from 'react';
|
import React, {Fragment, memo, useMemo} 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 {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';
|
||||||
@ -11,23 +10,29 @@ import settingsPageRouter from '_pages/settings/routing';
|
|||||||
import signInPageRouter from '_pages/sign-in/routing';
|
import signInPageRouter from '_pages/sign-in/routing';
|
||||||
import tagsPageRouter from '_pages/tags/routing';
|
import tagsPageRouter from '_pages/tags/routing';
|
||||||
import NotFoundPage from '_pages/not-found/components/page';
|
import NotFoundPage from '_pages/not-found/components/page';
|
||||||
|
import {useQuery} from '_hooks/useQuery';
|
||||||
import TopMenu from '../top-menu';
|
import TopMenu from '../top-menu';
|
||||||
import './Page.scss';
|
import './Page.scss';
|
||||||
import BothMenu from '../both-menu';
|
import BothMenu from '../both-menu';
|
||||||
|
import {queryParsers} from '../../utils';
|
||||||
|
import {ModalType} from '../../enums';
|
||||||
|
import CreateTaskModal from '../create-task-modal';
|
||||||
|
|
||||||
const useStyles = makeStyles(() =>
|
const useStyles = makeStyles(() =>
|
||||||
createStyles({
|
createStyles({
|
||||||
container: {
|
container: {
|
||||||
height: '100hv',
|
height: '100hv',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column'
|
||||||
},
|
}
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const Page: React.FC = () => {
|
const Page: React.FC = () => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const trigger = useScrollTrigger();
|
const trigger = useScrollTrigger();
|
||||||
|
const {modal} = useQuery(queryParsers);
|
||||||
|
const isOpenCreateTaskModal = useMemo(() => modal === ModalType.CreateTask, [modal]);
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className={classes.container}>
|
<div className={classes.container}>
|
||||||
@ -49,6 +54,7 @@ const Page: React.FC = () => {
|
|||||||
</Container>
|
</Container>
|
||||||
<BothMenu trigger={trigger} />
|
<BothMenu trigger={trigger} />
|
||||||
</div>
|
</div>
|
||||||
|
<CreateTaskModal isOpen={isOpenCreateTaskModal} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
24
src/app/components/popup-list-item/PopupListItem.tsx
Normal file
24
src/app/components/popup-list-item/PopupListItem.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {ListItem, ListItemText} from '@material-ui/core';
|
||||||
|
import React, {memo, useCallback} from 'react';
|
||||||
|
import {useHistory} from 'react-router-dom';
|
||||||
|
|
||||||
|
type PopupListItemProps = {
|
||||||
|
item: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PopupListItem: React.FC<PopupListItemProps> = ({item, url}) => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
history.push(url);
|
||||||
|
}, [history, url]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem button onClick={handleClick}>
|
||||||
|
<ListItemText primary={item} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(PopupListItem);
|
||||||
1
src/app/components/popup-list-item/index.ts
Normal file
1
src/app/components/popup-list-item/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export {default} from './PopupListItem';
|
||||||
@ -1,36 +1,39 @@
|
|||||||
import {Dialog, List, ListItem, ListItemText} from '@material-ui/core';
|
import {Dialog, List} from '@material-ui/core';
|
||||||
import React, {Fragment, memo, PropsWithChildren, useCallback} from 'react';
|
import React, {Fragment, memo, PropsWithChildren, useCallback, useMemo} from 'react';
|
||||||
import {v4} from 'uuid';
|
import {useHistory} from 'react-router-dom';
|
||||||
|
import {PageType} from '_enums/common';
|
||||||
|
import {buildPath} from '_utils/buildPath';
|
||||||
|
import {useQuery} from '_hooks/useQuery';
|
||||||
|
import {ModalType} from '../../../app/enums';
|
||||||
import {MENU_ADDS} from '../../consts';
|
import {MENU_ADDS} from '../../consts';
|
||||||
|
import PopupListItem from '../popup-list-item';
|
||||||
|
import {queryParsers} from '../../../app/utils';
|
||||||
|
|
||||||
type Props = PropsWithChildren<{
|
type Props = PropsWithChildren<{}>;
|
||||||
}>;
|
|
||||||
|
|
||||||
const PopupList: React.FC<Props> = ({children}) => {
|
const PopupList: React.FC<Props> = ({children}) => {
|
||||||
const [open, setOpen] = React.useState(false);
|
const {modal} = useQuery(queryParsers);
|
||||||
|
const isPopupListOpen = useMemo(() => modal === ModalType.CreateModal, [modal]);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const handleClickOpen = useCallback(() => {
|
const handleClickOpen = useCallback(() => {
|
||||||
setOpen(true);
|
history.push(buildPath({pageType: PageType.Main, query: {modal: ModalType.CreateModal}}));
|
||||||
}, [setOpen]);
|
}, [history]);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setOpen(false);
|
history.push(buildPath({pageType: PageType.Main}));
|
||||||
}, [setOpen]);
|
}, [history]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Dialog onClose={handleClose} aria-labelledby="simple-dialog-title" open={open}>
|
<Dialog onClose={handleClose} aria-labelledby="simple-dialog-title" open={isPopupListOpen}>
|
||||||
<List>
|
<List>
|
||||||
{MENU_ADDS.map(item => (
|
{MENU_ADDS.map(item => (
|
||||||
<ListItem button onClick={handleClose} key={v4()}>
|
<PopupListItem item={item.text} url={item.url} key={item.id} />
|
||||||
<ListItemText primary={item} />
|
|
||||||
</ListItem>
|
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<div onClick={handleClickOpen}>
|
<div onClick={handleClickOpen}>{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export {default} from './PopupList';
|
||||||
|
|||||||
@ -104,8 +104,9 @@ const TopMenu: React.FC<Props> = ({trigger}) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
edge="start"
|
edge="start"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
|
onClick={handleToggleSearch}
|
||||||
>
|
>
|
||||||
<SearchIcon onClick={handleToggleSearch} />
|
<SearchIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,22 @@
|
|||||||
|
import {v4} from 'uuid';
|
||||||
import {ROUTES} from '_consts/common';
|
import {ROUTES} from '_consts/common';
|
||||||
|
import {PageType} from '../core/enums/common';
|
||||||
|
import {buildPath} from '../core/utils/buildPath';
|
||||||
|
import {AddMenu, ModalType} from './enums';
|
||||||
|
|
||||||
export const LABELS = {
|
export const LABELS = {
|
||||||
SEACRH: 'Поиск',
|
SEACRH: 'Поиск',
|
||||||
ADD_TASK: 'Добавить задачу',
|
ADD_TASK: 'Добавить задачу',
|
||||||
ADD_FOLDER: 'Добавить папку',
|
ADD_FOLDER: 'Добавить папку',
|
||||||
ADD_TAG: 'Добавить тег',
|
ADD_TAG: 'Добавить тег',
|
||||||
|
ADD_ICON: 'Добавить иконку',
|
||||||
|
CREATE_TASK: 'Создание задачи',
|
||||||
|
CREATE: 'Создать',
|
||||||
|
CANCEL: 'Отмена',
|
||||||
|
TITLE: 'Заголовок',
|
||||||
|
DESCRIPTION: 'Описание',
|
||||||
|
START_AT: 'Начало',
|
||||||
|
END_AT: 'Окончание'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const BOTH_MENU_LINKS = [
|
export const BOTH_MENU_LINKS = [
|
||||||
@ -22,4 +34,23 @@ export const BOTH_MENU_LINKS = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MENU_ADDS = [LABELS.ADD_TASK, LABELS.ADD_FOLDER, LABELS.ADD_TAG];
|
export const MENU_ADDS = [
|
||||||
|
{
|
||||||
|
id: v4(),
|
||||||
|
text: LABELS.ADD_TASK,
|
||||||
|
type: AddMenu.AddTask,
|
||||||
|
url: buildPath({pageType: PageType.Main, query: {modal: ModalType.CreateTask}})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: v4(),
|
||||||
|
text: LABELS.ADD_FOLDER,
|
||||||
|
type: AddMenu.AddFolder,
|
||||||
|
url: buildPath({pageType: PageType.Main, query: {modal: ModalType.CreateFolder}})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: v4(),
|
||||||
|
text: LABELS.ADD_TAG,
|
||||||
|
type: AddMenu.AddTag,
|
||||||
|
url: buildPath({pageType: PageType.Main, query: {modal: ModalType.CreateTag}})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|||||||
12
src/app/enums.ts
Normal file
12
src/app/enums.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export enum ModalType {
|
||||||
|
CreateTask = 'createTask',
|
||||||
|
CreateFolder = 'createFolder',
|
||||||
|
CreateTag = 'createTag',
|
||||||
|
CreateModal = 'addModal'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AddMenu {
|
||||||
|
AddTask = 'addTask',
|
||||||
|
AddFolder = 'addFolder',
|
||||||
|
AddTag = 'addTag'
|
||||||
|
}
|
||||||
5
src/app/types.ts
Normal file
5
src/app/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import {ModalType} from './enums';
|
||||||
|
|
||||||
|
export type QueryParams = {
|
||||||
|
modal?: ModalType;
|
||||||
|
};
|
||||||
7
src/app/utils.ts
Normal file
7
src/app/utils.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import {QueryParsers} from '_utils/getQueryFromUrl';
|
||||||
|
import {stringParser} from '_utils/queryParsers';
|
||||||
|
import {QueryParams} from './types';
|
||||||
|
|
||||||
|
export const queryParsers: QueryParsers<QueryParams> = {
|
||||||
|
modal: stringParser(),
|
||||||
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import {makeApi} from '_utils/makeApi';
|
import {makeApi} from '_utils/makeApi';
|
||||||
import {http} from '_infrastructure/Http';
|
import {http} from '_infrastructure/Http';
|
||||||
|
|
||||||
type User = {
|
export type User = {
|
||||||
id: number;
|
id: number;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@ -21,3 +21,5 @@ export const PAGE_TITLE = {
|
|||||||
[PageType.Settings]: 'Settings',
|
[PageType.Settings]: 'Settings',
|
||||||
[PageType.SigIn]: 'SigIn',
|
[PageType.SigIn]: 'SigIn',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const VIEW_DATE_TIME = 'yyyy-MM-dd\'T\'HH:mm';
|
||||||
|
|||||||
@ -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 {createStore} from '_utils/createStore';
|
||||||
import {makeLocalStorageService} from './LocalStorageService';
|
import {makeLocalStorageService} from './LocalStorageService';
|
||||||
|
|
||||||
const TASK_STORAGE_NAME = 'FYB_TASK_STORAGE';
|
const TASK_STORAGE_NAME = 'FYB_TASK_STORAGE';
|
||||||
@ -32,5 +32,5 @@ const INIT_TASKS: Task[] = [
|
|||||||
|
|
||||||
const taskListService = makeLocalStorageService(INIT_TASKS, TASK_STORAGE_NAME);
|
const taskListService = makeLocalStorageService(INIT_TASKS, TASK_STORAGE_NAME);
|
||||||
|
|
||||||
export const tasksService = createService(taskListService.get(), {
|
export const tasksService = createStore(taskListService.get(), {
|
||||||
});
|
});
|
||||||
|
|||||||
4
src/core/types/LiveData.ts
Normal file
4
src/core/types/LiveData.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import {RemoteData} from '@devexperts/remote-data-ts';
|
||||||
|
import {Stream} from '@most/types';
|
||||||
|
|
||||||
|
export type LiveData<A, B> = Stream<RemoteData<A, B>>;
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import React, {ReactNode} from 'react';
|
import React, {ReactNode} from 'react';
|
||||||
import {RemoteData, fold, map as remoteDateMap} from '@devexperts/remote-data-ts';
|
import {RemoteData, fold, map as remoteDataMap, isSuccess} from '@devexperts/remote-data-ts';
|
||||||
import {Stream} from '@most/types';
|
import {Stream} from '@most/types';
|
||||||
import {map} from '@most/core';
|
import {chain, map, now, tap} from '@most/core';
|
||||||
import {pipe} from 'fp-ts/lib/pipeable';
|
import {pipe} from 'fp-ts/lib/pipeable';
|
||||||
|
import {LiveData} from '_types/LiveData';
|
||||||
|
|
||||||
export const renderAsyncData = <E, A>(
|
export const renderAsyncData = <E, A>(
|
||||||
data: RemoteData<E, A>,
|
data: RemoteData<E, A>,
|
||||||
@ -20,7 +21,32 @@ export const mapRD = <E, A, R>(mapper: (val: A) => R) => {
|
|||||||
return (stream$: Stream<RemoteData<E, A>>): Stream<RemoteData<E, R>> => {
|
return (stream$: Stream<RemoteData<E, A>>): Stream<RemoteData<E, R>> => {
|
||||||
return pipe(
|
return pipe(
|
||||||
stream$,
|
stream$,
|
||||||
map(val => remoteDateMap(mapper)(val))
|
map(val => remoteDataMap(mapper)(val))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tapRD = <E, A>(mapper: (val: A) => void) => {
|
||||||
|
return (stream$: Stream<RemoteData<E, A>>) => {
|
||||||
|
return pipe(
|
||||||
|
stream$,
|
||||||
|
tap(val => remoteDataMap(mapper)(val))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chainRD = <E, A, R>(chainer: (val: A) => Stream<RemoteData<E, R>>) => {
|
||||||
|
return (stream$: Stream<RemoteData<E, A>>) => {
|
||||||
|
return pipe(
|
||||||
|
stream$,
|
||||||
|
chain(rVal => {
|
||||||
|
if (isSuccess(rVal)) {
|
||||||
|
return chainer(rVal.value);
|
||||||
|
}
|
||||||
|
const res: LiveData<E, R> = now(rVal);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,7 +13,7 @@ type ServiceActions<State, T extends Record<string, ServiceAction<State, unknown
|
|||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
export const createService = <State, Actions extends Record<string, ServiceAction<State, any>>>(
|
export const createStore = <State, Actions extends Record<string, ServiceAction<State, any>>>(
|
||||||
initData: State,
|
initData: State,
|
||||||
actions: Actions
|
actions: Actions
|
||||||
) => {
|
) => {
|
||||||
23
src/core/utils/createSubject.ts
Normal file
23
src/core/utils/createSubject.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {createAdapter} from '@most/adapter';
|
||||||
|
import {Stream} from '@most/types';
|
||||||
|
import {hold} from '@most/hold';
|
||||||
|
|
||||||
|
export type Subject<T> = {
|
||||||
|
stream$: Stream<T>;
|
||||||
|
next: (val: T) => void;
|
||||||
|
getValue: () => T;
|
||||||
|
}
|
||||||
|
export const createSubject = <T>(data: T): Subject<T> => {
|
||||||
|
let cache = data;
|
||||||
|
|
||||||
|
const [handler, stream$] = createAdapter<T>();
|
||||||
|
|
||||||
|
return {
|
||||||
|
next: (val: T) => {
|
||||||
|
cache = val;
|
||||||
|
handler(val);
|
||||||
|
},
|
||||||
|
stream$: hold(stream$),
|
||||||
|
getValue: () => cache
|
||||||
|
};
|
||||||
|
};
|
||||||
149
src/core/utils/entity-store.ts
Normal file
149
src/core/utils/entity-store.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import {RemoteData, remoteData, isSuccess, success} from '@devexperts/remote-data-ts';
|
||||||
|
import {filter, map, skipRepeats, chain, tap, now} from '@most/core';
|
||||||
|
import {hold} from '@most/hold';
|
||||||
|
import {newDefaultScheduler} from '@most/scheduler';
|
||||||
|
import {array} from 'fp-ts/lib/Array';
|
||||||
|
import {Predicate} from 'fp-ts/lib/function';
|
||||||
|
import {pipe} from 'fp-ts/pipeable';
|
||||||
|
import {noop} from 'lodash';
|
||||||
|
|
||||||
|
import {isNotEmpty} from '_referers/common';
|
||||||
|
import {LiveData} from '_types/LiveData';
|
||||||
|
|
||||||
|
import {chainRD, mapRD, tapRD} from './asyncDataUtils';
|
||||||
|
import {StreamMap} from './streamMap';
|
||||||
|
|
||||||
|
export class EntityStore<L = never, A = never> {
|
||||||
|
get allValues$() {
|
||||||
|
return this._getAllValues$;
|
||||||
|
}
|
||||||
|
|
||||||
|
set allValues$(value: any) {
|
||||||
|
this._getAllValues$ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly cache = new StreamMap<string, RemoteData<L, A>>();
|
||||||
|
|
||||||
|
private readonly cachedStreams = new Map<string, LiveData<L, A>>();
|
||||||
|
|
||||||
|
private hasLoadedAll = false;
|
||||||
|
|
||||||
|
private isLoadingAll = false;
|
||||||
|
|
||||||
|
private _getAllValues$ = pipe(
|
||||||
|
this.cache.values$,
|
||||||
|
filter(() => !this.isLoadingAll && this.hasLoadedAll),
|
||||||
|
map(data => data.filter(item => isSuccess(item))),
|
||||||
|
map(array.sequence(remoteData)),
|
||||||
|
skipRepeats,
|
||||||
|
hold
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly keys$ = this.cache.keys$;
|
||||||
|
|
||||||
|
get(key: string, get: () => LiveData<L, A>): LiveData<L, A> {
|
||||||
|
let sharedGetter = this.cachedStreams.get(key);
|
||||||
|
|
||||||
|
if (!isNotEmpty(sharedGetter)) {
|
||||||
|
const hasValue = this.cache.has(key);
|
||||||
|
const cachedValue = this.cache.getValue(key);
|
||||||
|
const valueIsResolved = isNotEmpty(cachedValue) && isSuccess(cachedValue);
|
||||||
|
if (hasValue && valueIsResolved) {
|
||||||
|
return this.cache.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedGetter = pipe(get(), hold);
|
||||||
|
|
||||||
|
this.cachedStreams.set(key, sharedGetter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sharedGetter;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(
|
||||||
|
personalKey: (value: A) => string,
|
||||||
|
partialGetAll: () => LiveData<L, A[]>,
|
||||||
|
predicate?: Predicate<A>
|
||||||
|
): LiveData<L, A[]> {
|
||||||
|
this.isLoadingAll = false;
|
||||||
|
return pipe(
|
||||||
|
partialGetAll(),
|
||||||
|
tapRD(values => {
|
||||||
|
this.hasLoadedAll = true;
|
||||||
|
this.updateCache(values, personalKey);
|
||||||
|
}),
|
||||||
|
chain(data => {
|
||||||
|
this.cache.values$.run(
|
||||||
|
{
|
||||||
|
event: noop,
|
||||||
|
end: noop,
|
||||||
|
error: noop
|
||||||
|
},
|
||||||
|
newDefaultScheduler()
|
||||||
|
);
|
||||||
|
return isSuccess(data) ? this._getAllValues$ : now(data);
|
||||||
|
}),
|
||||||
|
skipRepeats,
|
||||||
|
mapRD(entities => {
|
||||||
|
if (typeof predicate === 'undefined') {
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
let hasChanges = false;
|
||||||
|
const filtered = entities.filter(value => {
|
||||||
|
const result = predicate(value);
|
||||||
|
if (!result) {
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
return hasChanges ? filtered : entities;
|
||||||
|
}),
|
||||||
|
hold
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(
|
||||||
|
key: string,
|
||||||
|
pk: (value: A) => string,
|
||||||
|
remove: () => LiveData<L, A[]>,
|
||||||
|
optimistic = true
|
||||||
|
): LiveData<L, A[]> {
|
||||||
|
if (optimistic) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
return pipe(
|
||||||
|
remove(),
|
||||||
|
tapRD(values => {
|
||||||
|
this.updateCache(values, pk);
|
||||||
|
}),
|
||||||
|
chain(() => this._getAllValues$)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(personalKey: (value: A) => string, create: () => LiveData<L, A>): LiveData<L, A> {
|
||||||
|
return pipe(
|
||||||
|
create(),
|
||||||
|
chainRD(value => {
|
||||||
|
const key = personalKey(value);
|
||||||
|
this.cache.set(key, success(value));
|
||||||
|
return this.cache.get(key);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(key: string, update: () => LiveData<L, A>): LiveData<L, A> {
|
||||||
|
return pipe(
|
||||||
|
update(),
|
||||||
|
tap(value => {
|
||||||
|
if (isSuccess(value)) {
|
||||||
|
this.cache.set(key, value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCache(values: A[], pk: (value: A) => string): void {
|
||||||
|
const entries = values.map<[string, RemoteData<L, A>]>(item => [pk(item), success(item)]);
|
||||||
|
this.cache.setMany(entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,20 +1,33 @@
|
|||||||
import {fromPromise} from '@most/core';
|
import {failure, pending, RemoteData, success} from '@devexperts/remote-data-ts';
|
||||||
|
import {fromPromise, startWith, recoverWith, map, now} from '@most/core';
|
||||||
import {Stream} from '@most/types';
|
import {Stream} from '@most/types';
|
||||||
|
import {pipe} from 'fp-ts/pipeable';
|
||||||
import {objectEntries} from './objectEntries';
|
import {objectEntries} from './objectEntries';
|
||||||
|
|
||||||
type PromiseApi = Record<string, (...args: any[]) => Promise<unknown>>;
|
type PromiseApi = Record<string, (...args: any[]) => Promise<unknown>>;
|
||||||
|
|
||||||
type StreamApi<T extends PromiseApi> = {
|
type StreamApi<T extends PromiseApi, E> = {
|
||||||
[K in keyof T]: (...params: Parameters<T[K]>) => (
|
[K in keyof T]: (
|
||||||
T[K] extends (...args: any[]) => Promise<infer R> ? Stream<R> : never
|
...params: Parameters<T[K]>
|
||||||
);
|
) => T[K] extends (...args: any[]) => Promise<infer R> ? Stream<RemoteData<E, R>> : never;
|
||||||
};
|
};
|
||||||
|
const pipeApiStream = <T>(stream$: Stream<T>) =>
|
||||||
|
pipe(
|
||||||
|
stream$,
|
||||||
|
map(val => success(val)),
|
||||||
|
startWith(pending),
|
||||||
|
recoverWith(err => now(failure(err)))
|
||||||
|
);
|
||||||
|
|
||||||
export const makeApi = <T extends PromiseApi>(apiObj: T) => {
|
export const makeApi = <T extends PromiseApi, E = Error>(apiObj: T) => {
|
||||||
return objectEntries(apiObj).reduce((streamObj, [apiKey, apiMethod]) => {
|
return objectEntries(apiObj).reduce((streamObj, [apiKey, apiMethod]) => {
|
||||||
return {
|
return {
|
||||||
...streamObj,
|
...streamObj,
|
||||||
[apiKey]: (...args: Parameters<typeof apiMethod>) => fromPromise(apiMethod(...args)),
|
[apiKey]: (...args: Parameters<typeof apiMethod>) => {
|
||||||
|
const res = fromPromise(apiMethod(...args));
|
||||||
|
|
||||||
|
return pipeApiStream(res);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, {} as StreamApi<T>);
|
}, {} as StreamApi<T, E>);
|
||||||
};
|
};
|
||||||
|
|||||||
185
src/core/utils/streamMap.ts
Normal file
185
src/core/utils/streamMap.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import {skipRepeats, filter, map} from '@most/core';
|
||||||
|
import {hold} from '@most/hold';
|
||||||
|
import {Stream} from '@most/types';
|
||||||
|
import {pipe} from 'fp-ts/pipeable';
|
||||||
|
|
||||||
|
import {createSubject, Subject} from '_utils/createSubject';
|
||||||
|
|
||||||
|
import {isNotEmpty} from '../referers/common';
|
||||||
|
|
||||||
|
type UninitializedEntity<V> = {
|
||||||
|
hasValue: false;
|
||||||
|
subject: Subject<V | undefined>;
|
||||||
|
stream: Stream<V>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type InitializedEntity<V> = {
|
||||||
|
hasValue: true;
|
||||||
|
subject: Subject<V>;
|
||||||
|
stream: Stream<V>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StreamMapEntity<V> = UninitializedEntity<V> | InitializedEntity<V>;
|
||||||
|
|
||||||
|
type InitializedEntry<K, V> = [K, InitializedEntity<V>];
|
||||||
|
type StreamMapEntry<K, V> = [K, StreamMapEntity<V>];
|
||||||
|
|
||||||
|
export class StreamMap<K, V> {
|
||||||
|
private cache = new Map<K, StreamMapEntity<V>>();
|
||||||
|
|
||||||
|
private allSubject$ = createSubject<void>(undefined);
|
||||||
|
|
||||||
|
private isInTransaction = false;
|
||||||
|
|
||||||
|
private hasChanges = false;
|
||||||
|
|
||||||
|
private _keys$ = createSubject<K[]>([]);
|
||||||
|
|
||||||
|
readonly keys$ = this._keys$.stream$;
|
||||||
|
|
||||||
|
readonly values$ = pipe(this.allSubject$.stream$,
|
||||||
|
map(() => {
|
||||||
|
const values = Array.from(this.cache.values());
|
||||||
|
return values.filter(isInitialized).map(entity => entity.subject.getValue());
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly entries$: Stream<[K, V][]> = pipe(
|
||||||
|
this.allSubject$.stream$,
|
||||||
|
map(() => {
|
||||||
|
const entries = Array.from(this.cache.entries()).filter(isEntryInitialized);
|
||||||
|
return entries.map<[K, V]>(entry => [entry[0], entry[1].subject.getValue()]);
|
||||||
|
}),
|
||||||
|
hold,
|
||||||
|
);
|
||||||
|
|
||||||
|
get size(): number {
|
||||||
|
return this.cache.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isEmpty(): boolean {
|
||||||
|
return this.size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleKeys = (keys: K[]) => this._keys$.next(keys);
|
||||||
|
|
||||||
|
has(key: K): boolean {
|
||||||
|
return this.cache.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: K): Stream<V> {
|
||||||
|
return this.getOrCreateCached(key).stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue(key: K): V | undefined {
|
||||||
|
if (this.cache.has(key)) {
|
||||||
|
const value = this.cache.get(key);
|
||||||
|
if (isNotEmpty(value) && value.hasValue) {
|
||||||
|
return value.subject.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: K, value: V): void {
|
||||||
|
this.transaction(() => {
|
||||||
|
const isCachedKey = this.cache.has(key);
|
||||||
|
let cached = this.getOrCreateCached(key);
|
||||||
|
if (cached.hasValue === false) {
|
||||||
|
cached = initializeEntity(cached);
|
||||||
|
this.cache.set(key, cached);
|
||||||
|
if (!isCachedKey) {
|
||||||
|
this.handleKeys(Array.from(this.cache.keys()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cached.subject.getValue() !== value) {
|
||||||
|
cached.subject.next(value);
|
||||||
|
this.hasChanges = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setMany(entries: [K, V][]): void {
|
||||||
|
this.transaction(() => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
const [key, value] = entry;
|
||||||
|
this.set(key, value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction(thunk: () => void): void {
|
||||||
|
if (!this.isInTransaction) {
|
||||||
|
this.isInTransaction = true;
|
||||||
|
thunk();
|
||||||
|
if (this.hasChanges) {
|
||||||
|
this.hasChanges = false;
|
||||||
|
this.allSubject$.next(undefined);
|
||||||
|
}
|
||||||
|
this.isInTransaction = false;
|
||||||
|
} else {
|
||||||
|
// Execute the thunk, notifications will be handled by parent transaction
|
||||||
|
thunk();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key: K): void {
|
||||||
|
this.transaction(() => {
|
||||||
|
const isCachedKey = this.cache.has(key);
|
||||||
|
this.cache.delete(key);
|
||||||
|
if (isCachedKey) {
|
||||||
|
this.handleKeys(Array.from(this.cache.keys()));
|
||||||
|
}
|
||||||
|
this.hasChanges = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteMany(keys: K[]): void {
|
||||||
|
this.transaction(() => {
|
||||||
|
keys.forEach(key => {
|
||||||
|
this.delete(key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.transaction(() => {
|
||||||
|
this.cache.clear();
|
||||||
|
this.handleKeys(Array.from(this.cache.keys()));
|
||||||
|
this.hasChanges = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOrCreateCached(key: K): StreamMapEntity<V> {
|
||||||
|
const cached = this.cache.get(key);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subject = createSubject<V | undefined>(undefined);
|
||||||
|
const stream = pipe(subject.stream$, filter(isNotEmpty), skipRepeats);
|
||||||
|
const entity: StreamMapEntity<V> = {
|
||||||
|
hasValue: false,
|
||||||
|
subject,
|
||||||
|
stream,
|
||||||
|
};
|
||||||
|
this.cache.set(key, entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeEntity<V>(entity: UninitializedEntity<V>): InitializedEntity<V> {
|
||||||
|
return {
|
||||||
|
...entity,
|
||||||
|
subject: entity.subject as Subject<V>,
|
||||||
|
hasValue: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInitialized<V>(entity: StreamMapEntity<V>): entity is InitializedEntity<V> {
|
||||||
|
return entity.hasValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEntryInitialized<K, V>(entry: StreamMapEntry<K, V>): entry is InitializedEntry<K, V> {
|
||||||
|
return isInitialized(entry[1]);
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import {Sink, Stream} from '@most/types';
|
|||||||
import {noop} from 'lodash';
|
import {noop} from 'lodash';
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {newDefaultScheduler} from '@most/scheduler';
|
import {newDefaultScheduler} from '@most/scheduler';
|
||||||
|
import {pending, RemoteData} from '@devexperts/remote-data-ts';
|
||||||
|
|
||||||
export function useStream<T extends Array<unknown>, R>(
|
export function useStream<T extends Array<unknown>, R>(
|
||||||
piping: () => Stream<R>,
|
piping: () => Stream<R>,
|
||||||
@ -31,3 +32,32 @@ export function useStream<T extends Array<unknown>, R>(
|
|||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useStreamRD<T extends Array<unknown>, R, E = Error>(
|
||||||
|
piping: () => Stream<RemoteData<E, R>>,
|
||||||
|
props: T,
|
||||||
|
) {
|
||||||
|
const [state, setState] = useState<RemoteData<E, R>>(pending);
|
||||||
|
useEffect(() => {
|
||||||
|
setState(pending);
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, props);
|
||||||
|
useEffect(() => {
|
||||||
|
const effect$ = piping();
|
||||||
|
const sink: Sink<RemoteData<E, R>> = {
|
||||||
|
event: (_, val) => {
|
||||||
|
setState(val);
|
||||||
|
},
|
||||||
|
end: noop,
|
||||||
|
error: noop
|
||||||
|
};
|
||||||
|
const unsub = effect$.run(sink, newDefaultScheduler());
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsub.dispose();
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, props);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,22 +1,35 @@
|
|||||||
import {combine} from '@most/core';
|
import {combine} from '@most/core';
|
||||||
|
import {combine as combineRD, map} from '@devexperts/remote-data-ts';
|
||||||
|
import {pipe} from 'fp-ts/lib/function';
|
||||||
import React, {Fragment, memo} from 'react';
|
import React, {Fragment, memo} from 'react';
|
||||||
import {isNotEmpty} from '_referers/common';
|
|
||||||
import {makeTreeList} from '_utils/makeTreeList';
|
|
||||||
import {commonApi} from '_api/commonApi';
|
import {commonApi} from '_api/commonApi';
|
||||||
import {useStream} from '_utils/useStream';
|
import {renderAsyncData} from '_utils/asyncDataUtils';
|
||||||
|
import {makeTreeList} from '_utils/makeTreeList';
|
||||||
|
import {useStreamRD} from '_utils/useStream';
|
||||||
|
|
||||||
import InfoList from '../info-list';
|
import InfoList from '../info-list';
|
||||||
|
|
||||||
const stream$ = combine((taskList, folderList) => {
|
const stream$ = combine(
|
||||||
return makeTreeList(folderList, taskList);
|
(taskListRD, folderListRD) => {
|
||||||
}, commonApi.taskList.getAll(), commonApi.folderList.getAll());
|
return pipe(
|
||||||
|
combineRD(taskListRD, folderListRD),
|
||||||
|
map(([taskList, folderList]) => {
|
||||||
|
return makeTreeList(folderList, taskList);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
commonApi.taskList.getAll(),
|
||||||
|
commonApi.folderList.getAll()
|
||||||
|
);
|
||||||
|
|
||||||
const Page: React.FC = () => {
|
const Page: React.FC = () => {
|
||||||
const tree = useStream(() => stream$, []);
|
const treeRD = useStreamRD(() => stream$, []);
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{isNotEmpty(tree) && (
|
{renderAsyncData(treeRD, tree => (
|
||||||
<InfoList list={tree} space={1} />
|
<InfoList list={tree} space={1} />
|
||||||
)}
|
))}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,23 +1,29 @@
|
|||||||
import React, {memo} from 'react';
|
import React, {Fragment, memo} from 'react';
|
||||||
import {Link} from 'react-router-dom';
|
import {renderAsyncData} from '_utils/asyncDataUtils';
|
||||||
import {usersApi} from '_api/usersTestApi';
|
|
||||||
import {useStream} from '_utils/useStream';
|
|
||||||
|
|
||||||
const userList$ = usersApi.request();
|
import {usersApi} from '_api/usersTestApi';
|
||||||
|
import {useStreamRD} from '_utils/useStream';
|
||||||
|
|
||||||
|
import UserComponent from './User';
|
||||||
|
|
||||||
const Page: React.FC = () => {
|
const Page: React.FC = () => {
|
||||||
const users = useStream(() => userList$, []);
|
const users = useStreamRD(() => usersApi.request(), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Fragment>
|
||||||
tags
|
<div>
|
||||||
{users?.map(user => (
|
tags
|
||||||
<div key={user.id}>
|
{renderAsyncData(users, successData =>
|
||||||
{user.first_name}, {user.last_name}
|
successData?.map(user => <UserComponent userId={user.id} key={user.id} />)
|
||||||
<span><Link to={`/tags/${user.id}`}> More info...</Link></span>
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div>
|
||||||
</div>
|
tags
|
||||||
|
{renderAsyncData(users, successData =>
|
||||||
|
successData?.map(user => <UserComponent userId={user.id} key={user.id} />)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
37
src/pages/tags/components/page/User.tsx
Normal file
37
src/pages/tags/components/page/User.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React, {FC, Fragment, memo} from 'react';
|
||||||
|
import {Link} from 'react-router-dom';
|
||||||
|
|
||||||
|
import {usersApi} from '_api/usersTestApi';
|
||||||
|
import {renderAsyncData} from '_utils/asyncDataUtils';
|
||||||
|
import {useStreamRD} from '_utils/useStream';
|
||||||
|
import {userEntityStore} from './utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
userId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const User: FC<Props> = ({userId}) => {
|
||||||
|
const data =
|
||||||
|
useStreamRD(() => {
|
||||||
|
const userStringId = userId.toString();
|
||||||
|
|
||||||
|
return userEntityStore.get(userStringId, () =>
|
||||||
|
usersApi.findById(userStringId)
|
||||||
|
);
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{renderAsyncData(data, user => (
|
||||||
|
<div key={user.id}>
|
||||||
|
{user.first_name}, {user.last_name}
|
||||||
|
<span>
|
||||||
|
<Link to={`/tags/${user.id}`}> More info...</Link>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(User);
|
||||||
4
src/pages/tags/components/page/utils.ts
Normal file
4
src/pages/tags/components/page/utils.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import {User} from '_api/usersTestApi';
|
||||||
|
import {EntityStore} from '_utils/entity-store';
|
||||||
|
|
||||||
|
export const userEntityStore = new EntityStore<Error, User>();
|
||||||
@ -1,38 +1,44 @@
|
|||||||
import React, {FC, memo} from 'react';
|
import {success} from '@devexperts/remote-data-ts';
|
||||||
import {pipe} from 'fp-ts/es6/pipeable';
|
|
||||||
import {at, chain, periodic, map} from '@most/core';
|
import {at, chain, periodic, map} from '@most/core';
|
||||||
|
import {pipe} from 'fp-ts/es6/pipeable';
|
||||||
import {useStream} from '_utils/useStream';
|
import React, {FC, memo} from 'react';
|
||||||
import {usersApi} from '_api/usersTestApi';
|
|
||||||
import {useParams} from 'react-router-dom';
|
import {useParams} from 'react-router-dom';
|
||||||
|
|
||||||
|
import {usersApi} from '_api/usersTestApi';
|
||||||
|
import {chainRD, renderAsyncData} from '_utils/asyncDataUtils';
|
||||||
|
import {useStreamRD} from '_utils/useStream';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const User: FC<Props> = () => {
|
const User: FC<Props> = () => {
|
||||||
const {id} = useParams<Props>();
|
const {id} = useParams<Props>();
|
||||||
|
|
||||||
const user = useStream(() => {
|
const userRD = useStreamRD(() => {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
return pipe(
|
return pipe(
|
||||||
at(3000, undefined),
|
at(3000, undefined),
|
||||||
chain(() => usersApi.findById(id)),
|
chain(() => usersApi.findById(id)),
|
||||||
chain(data => {
|
chainRD(data => {
|
||||||
return pipe(periodic(1000), map(() => {
|
const res = pipe(
|
||||||
i = i + 1;
|
periodic(1000),
|
||||||
return {
|
map(() => {
|
||||||
...data,
|
i = i + 1;
|
||||||
chainableNumber: i
|
return success({
|
||||||
};
|
...data,
|
||||||
}));
|
chainableNumber: i
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return res;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{user ? (
|
{renderAsyncData(userRD, user => (
|
||||||
<div>
|
<div>
|
||||||
<div>{user.avatar}</div>
|
<div>{user.avatar}</div>
|
||||||
<div>{user.email}</div>
|
<div>{user.email}</div>
|
||||||
@ -41,9 +47,7 @@ const User: FC<Props> = () => {
|
|||||||
<div>{user.last_name}</div>
|
<div>{user.last_name}</div>
|
||||||
<div>{user.chainableNumber}</div>
|
<div>{user.chainableNumber}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
))}
|
||||||
'Loading...'
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user