1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 20:36:40 +07:00

#58 fixed dialog routers

This commit is contained in:
Fedor Katurov 2021-03-29 17:45:54 +07:00
parent 3e8c2d4b6e
commit 124719c243
15 changed files with 93 additions and 39 deletions

View file

@ -16,22 +16,29 @@ const SubmitBar: FC<Props> = ({ isLab }) => {
const onFocus = useCallback(() => setFocused(true), [setFocused]); const onFocus = useCallback(() => setFocused(true), [setFocused]);
const onBlur = useCallback(() => setFocused(false), [setFocused]); const onBlur = useCallback(() => setFocused(false), [setFocused]);
const createUrl = useCallback(
(type: string) => {
return [url.replace(/\/$/, ''), 'create', type].join('/');
},
[url]
);
return ( return (
<div className={classNames(styles.wrap, { [styles.lab]: isLab })}> <div className={classNames(styles.wrap, { [styles.lab]: isLab })}>
<div className={classNames(styles.panel, { [styles.active]: focused })}> <div className={classNames(styles.panel, { [styles.active]: focused })}>
<Link to={`${url}/create/image`} className={styles.link}> <Link to={createUrl('image')} className={styles.link}>
<Icon icon="image" size={32} /> <Icon icon="image" size={32} />
</Link> </Link>
<Link to={`${url}/create/text`} className={styles.link}> <Link to={createUrl('text')} className={styles.link}>
<Icon icon="text" size={32} /> <Icon icon="text" size={32} />
</Link> </Link>
<Link to={`${url}/create/video`} className={styles.link}> <Link to={createUrl('video')} className={styles.link}>
<Icon icon="video" size={32} /> <Icon icon="video" size={32} />
</Link> </Link>
<Link to={`${url}/create/audio`} className={styles.link}> <Link to={createUrl('audio')} className={styles.link}>
<Icon icon="audio" size={32} /> <Icon icon="audio" size={32} />
</Link> </Link>
</div> </div>

View file

@ -7,7 +7,7 @@ import { Padder } from '~/components/containers/Padder';
import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik'; import { useNodeFormContext } from '~/utils/hooks/useNodeFormFormik';
const EditorButtons: FC = () => { const EditorButtons: FC = () => {
const { values, handleChange } = useNodeFormContext(); const { values, handleChange, isSubmitting } = useNodeFormContext();
return ( return (
<Padder style={{ position: 'relative' }}> <Padder style={{ position: 'relative' }}>
@ -20,12 +20,14 @@ const EditorButtons: FC = () => {
handler={handleChange('title')} handler={handleChange('title')}
autoFocus autoFocus
maxLength={256} maxLength={256}
disabled={isSubmitting}
/> />
<Button <Button
title="Сохранить" title="Сохранить"
iconRight="check" iconRight="check"
color={values.is_promoted ? 'primary' : 'lab'} color={values.is_promoted ? 'primary' : 'lab'}
disabled={isSubmitting}
/> />
</Group> </Group>
</Padder> </Padder>

View file

@ -40,8 +40,6 @@ const BetterScrollDialog: FC<IProps> = ({
return () => clearAllBodyScrollLocks(); return () => clearAllBodyScrollLocks();
}, [ref]); }, [ref]);
useCloseOnEscape(onClose);
return ( return (
<div className={styles.wrap} ref={ref}> <div className={styles.wrap} ref={ref}>
{backdrop && <div className={styles.backdrop}>{backdrop}</div>} {backdrop && <div className={styles.backdrop}>{backdrop}</div>}

View file

@ -104,6 +104,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 0 0 $radius $radius; border-radius: 0 0 $radius $radius;
z-index: 11;
} }
.backdrop { .backdrop {

View file

@ -12,7 +12,7 @@ const EditorCreateDialog: FC = () => {
} = useRouteMatch<{ type: string }>(); } = useRouteMatch<{ type: string }>();
const backUrl = useMemo(() => { const backUrl = useMemo(() => {
return url.replace(/\/create\/(.*)$/, ''); return (url && url.replace(/\/create\/(.*)$/, '')) || '/';
}, [url]); }, [url]);
const goBack = useCallback(() => { const goBack = useCallback(() => {

View file

@ -1,4 +1,4 @@
import React, { createElement, FC, useCallback, useMemo } from 'react'; import React, { createElement, FC, useCallback, useEffect, useMemo } from 'react';
import { IDialogProps } from '~/redux/modal/constants'; import { IDialogProps } from '~/redux/modal/constants';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { NODE_EDITORS } from '~/redux/node/constants'; import { NODE_EDITORS } from '~/redux/node/constants';
@ -12,26 +12,40 @@ import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
import { FormikProvider } from 'formik'; import { FormikProvider } from 'formik';
import { INode } from '~/redux/types'; import { INode } from '~/redux/types';
import { ModalWrapper } from '~/components/dialogs/ModalWrapper'; import { ModalWrapper } from '~/components/dialogs/ModalWrapper';
import { useTranslatedError } from '~/utils/hooks/useTranslatedError';
import { useCloseOnEscape } from '~/utils/hooks';
interface Props extends IDialogProps { interface Props extends IDialogProps {
node: INode; node: INode;
} }
const EditorDialog: FC<Props> = ({ node, onRequestClose }) => { const EditorDialog: FC<Props> = ({ node, onRequestClose }) => {
const uploader = useFileUploader(UPLOAD_SUBJECTS.EDITOR, UPLOAD_TARGETS.NODES, []); const uploader = useFileUploader(UPLOAD_SUBJECTS.EDITOR, UPLOAD_TARGETS.NODES, node.files);
const formik = useNodeFormFormik(node, uploader, onRequestClose); const formik = useNodeFormFormik(node, uploader, onRequestClose);
const { values, handleSubmit, dirty } = formik; const { values, handleSubmit, dirty, status, setStatus } = formik;
const component = useMemo(() => node.type && prop(node.type, NODE_EDITORS), [node.type]); const component = useMemo(() => node.type && prop(node.type, NODE_EDITORS), [node.type]);
const onClose = useCallback(() => { const onClose = useCallback(() => {
if (!window.confirm('Точно выйти?')) { if (!window.confirm('Точно выйти?')) {
return; return undefined;
} }
onRequestClose(); onRequestClose();
}, [onRequestClose, dirty]); }, [onRequestClose, dirty]);
const error = useTranslatedError(status);
useEffect(() => {
if (!status) {
return;
}
setStatus('');
}, [values]);
useCloseOnEscape(onClose);
if (!component) { if (!component) {
return null; return null;
} }
@ -45,7 +59,7 @@ const EditorDialog: FC<Props> = ({ node, onRequestClose }) => {
footer={<EditorButtons />} footer={<EditorButtons />}
backdrop={<CoverBackdrop cover={values.cover} />} backdrop={<CoverBackdrop cover={values.cover} />}
width={860} width={860}
error="" error={error}
onClose={onClose} onClose={onClose}
> >
<div className={styles.editor}>{createElement(component)}</div> <div className={styles.editor}>{createElement(component)}</div>

View file

@ -9,7 +9,7 @@ import styles from './styles.module.scss';
const EditorEditDialog: FC = () => { const EditorEditDialog: FC = () => {
const [data, setData] = useState(EMPTY_NODE); const [data, setData] = useState(EMPTY_NODE);
const [isLoading, setLoading] = useState(false); const [isLoading, setLoading] = useState(true);
const history = useHistory(); const history = useHistory();
const { const {

View file

@ -18,11 +18,11 @@ const MainRouter: FC<IProps> = () => {
return ( return (
<Switch location={location}> <Switch location={location}>
<Route exact path={URLS.BASE} component={FlowLayout} />
<Route path={URLS.NODE_URL(':id')} component={NodeLayout} /> <Route path={URLS.NODE_URL(':id')} component={NodeLayout} />
<Route path={URLS.BORIS} component={BorisLayout} /> <Route path={URLS.BORIS} component={BorisLayout} />
<Route path={URLS.ERRORS.NOT_FOUND} component={ErrorNotFound} /> <Route path={URLS.ERRORS.NOT_FOUND} component={ErrorNotFound} />
<Route path={URLS.PROFILE_PAGE(':username')} component={ProfilePage} /> <Route path={URLS.PROFILE_PAGE(':username')} component={ProfilePage} />
<Route path={URLS.BASE} component={FlowLayout} />
{is_user && ( {is_user && (
<> <>

View file

@ -87,7 +87,7 @@ const FlowLayoutUnconnected: FC<IProps> = ({
/> />
</div> </div>
<SidebarRouter prefix="/" /> <SidebarRouter prefix="" />
</Container> </Container>
); );
}; };

View file

@ -7,11 +7,6 @@ export const nodeSet = (node: Partial<INodeState>) => ({
type: NODE_ACTIONS.SET, type: NODE_ACTIONS.SET,
}); });
export const nodeSave = (node: INode) => ({
node,
type: NODE_ACTIONS.SAVE,
});
export const nodeSetSaveErrors = (errors: IValidationErrors) => ({ export const nodeSetSaveErrors = (errors: IValidationErrors) => ({
errors, errors,
type: NODE_ACTIONS.SET_SAVE_ERRORS, type: NODE_ACTIONS.SET_SAVE_ERRORS,
@ -55,6 +50,15 @@ export const nodePostLocalComment = (
type: NODE_ACTIONS.POST_COMMENT, type: NODE_ACTIONS.POST_COMMENT,
}); });
export const nodeSubmitLocal = (
node: INode,
callback: (e?: string, errors?: Record<string, string>) => void
) => ({
node,
callback,
type: NODE_ACTIONS.SUBMIT_LOCAL,
});
export const nodeSetSendingComment = (is_sending_comment: boolean) => ({ export const nodeSetSendingComment = (is_sending_comment: boolean) => ({
is_sending_comment, is_sending_comment,
type: NODE_ACTIONS.SET_SENDING_COMMENT, type: NODE_ACTIONS.SET_SENDING_COMMENT,

View file

@ -40,6 +40,9 @@ export type ApiGetNodeCommentsResponse = { comments: IComment[]; comment_count:
export const apiPostNode = ({ node }: ApiPostNodeRequest) => export const apiPostNode = ({ node }: ApiPostNodeRequest) =>
api.post<ApiPostNodeResult>(API.NODE.SAVE, node).then(cleanResult); api.post<ApiPostNodeResult>(API.NODE.SAVE, node).then(cleanResult);
export const apiPostNodeLocal = ({ node }: ApiPostNodeRequest) =>
api.post<ApiPostNodeResult>(API.NODE.SAVE, node).then(cleanResult);
export const getNodeDiff = ({ export const getNodeDiff = ({
start, start,
end, end,

View file

@ -18,12 +18,11 @@ import { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock';
import { LabNodeTitle } from '~/components/lab/LabNodeTitle'; import { LabNodeTitle } from '~/components/lab/LabNodeTitle';
import { LabText } from '~/components/lab/LabText'; import { LabText } from '~/components/lab/LabText';
import { LabImage } from '~/components/lab/LabImage'; import { LabImage } from '~/components/lab/LabImage';
import { LabBottomPanel } from '~/components/lab/LabBottomPanel';
import { LabPad } from '~/components/lab/LabPad'; import { LabPad } from '~/components/lab/LabPad';
const prefix = 'NODE.'; const prefix = 'NODE.';
export const NODE_ACTIONS = { export const NODE_ACTIONS = {
SAVE: `${prefix}SAVE`, SUBMIT_LOCAL: `${prefix}SUBMIT_LOCAL`,
LOAD_NODE: `${prefix}LOAD_NODE`, LOAD_NODE: `${prefix}LOAD_NODE`,
GOTO_NODE: `${prefix}GOTO_NODE`, GOTO_NODE: `${prefix}GOTO_NODE`,
SET: `${prefix}SET`, SET: `${prefix}SET`,

View file

@ -17,7 +17,6 @@ import {
nodeLock, nodeLock,
nodeLockComment, nodeLockComment,
nodePostLocalComment, nodePostLocalComment,
nodeSave,
nodeSet, nodeSet,
nodeSetCommentData, nodeSetCommentData,
nodeSetComments, nodeSetComments,
@ -26,8 +25,8 @@ import {
nodeSetLoading, nodeSetLoading,
nodeSetLoadingComments, nodeSetLoadingComments,
nodeSetRelated, nodeSetRelated,
nodeSetSaveErrors,
nodeSetTags, nodeSetTags,
nodeSubmitLocal,
nodeUpdateTags, nodeUpdateTags,
} from './actions'; } from './actions';
import { import {
@ -43,7 +42,6 @@ import {
apiPostNodeTags, apiPostNodeTags,
} from './api'; } from './api';
import { flowSetNodes, flowSetUpdated } from '../flow/actions'; import { flowSetNodes, flowSetUpdated } from '../flow/actions';
import { ERRORS } from '~/constants/errors';
import { modalSetShown, modalShowDialog } from '../modal/actions'; import { modalSetShown, modalShowDialog } from '../modal/actions';
import { selectFlow, selectFlowNodes } from '../flow/selectors'; import { selectFlow, selectFlowNodes } from '../flow/selectors';
import { URLS } from '~/constants/urls'; import { URLS } from '~/constants/urls';
@ -73,14 +71,12 @@ export function* updateNodeEverywhere(node) {
); );
} }
function* onNodeSave({ node }: ReturnType<typeof nodeSave>) { function* onNodeSubmitLocal({ node, callback }: ReturnType<typeof nodeSubmitLocal>) {
try { try {
yield put(nodeSetSaveErrors({}));
const { errors, node: result }: Unwrap<typeof apiPostNode> = yield call(apiPostNode, { node }); const { errors, node: result }: Unwrap<typeof apiPostNode> = yield call(apiPostNode, { node });
if (errors && Object.values(errors).length > 0) { if (errors && Object.values(errors).length > 0) {
yield put(nodeSetSaveErrors(errors)); callback('', errors);
return; return;
} }
@ -97,9 +93,10 @@ function* onNodeSave({ node }: ReturnType<typeof nodeSave>) {
yield put(nodeSetCurrent(result)); yield put(nodeSetCurrent(result));
} }
return yield put(modalSetShown(false)); callback();
return;
} catch (error) { } catch (error) {
yield put(nodeSetSaveErrors({ error: error.message || ERRORS.CANT_SAVE_NODE })); callback(error.message);
} }
} }
@ -361,7 +358,7 @@ function* onLockCommentSaga({ id, is_locked }: ReturnType<typeof nodeLockComment
} }
export default function* nodeSaga() { export default function* nodeSaga() {
yield takeLatest(NODE_ACTIONS.SAVE, onNodeSave); yield takeLatest(NODE_ACTIONS.SUBMIT_LOCAL, onNodeSubmitLocal);
yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto); yield takeLatest(NODE_ACTIONS.GOTO_NODE, onNodeGoto);
yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad); yield takeLatest(NODE_ACTIONS.LOAD_NODE, onNodeLoad);
yield takeLatest(NODE_ACTIONS.POST_COMMENT, onPostComment); yield takeLatest(NODE_ACTIONS.POST_COMMENT, onPostComment);

View file

@ -16,10 +16,10 @@ export const useCloseOnEscape = (onRequestClose?: () => void, ignore_inputs = fa
); );
useEffect(() => { useEffect(() => {
window.addEventListener('keyup', onEscape); document.addEventListener('keyup', onEscape);
return () => { return () => {
window.removeEventListener('keyup', onEscape); document.removeEventListener('keyup', onEscape);
}; };
}, [onEscape]); }, [onEscape]);
}; };

View file

@ -1,17 +1,46 @@
import { INode } from '~/redux/types'; import { IComment, INode } from '~/redux/types';
import { FileUploader } from '~/utils/hooks/fileUploader'; import { FileUploader } from '~/utils/hooks/fileUploader';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { useFormik, useFormikContext } from 'formik'; import { FormikHelpers, useFormik, useFormikContext } from 'formik';
import { object } from 'yup'; import { object, string } from 'yup';
import { useDispatch } from 'react-redux';
import { nodeSubmitLocal } from '~/redux/node/actions';
import { keys } from 'ramda';
const validationSchema = object().shape({}); const validationSchema = object().shape({});
const onSuccess = ({ resetForm, setStatus, setSubmitting, setErrors }: FormikHelpers<INode>) => (
e?: string,
errors?: Record<string, string>
) => {
setSubmitting(false);
if (e) {
setStatus(e);
return;
}
if (errors && keys(errors).length) {
setErrors(errors);
return;
}
if (resetForm) {
resetForm();
}
};
export const useNodeFormFormik = ( export const useNodeFormFormik = (
values: INode, values: INode,
uploader: FileUploader, uploader: FileUploader,
stopEditing: () => void stopEditing: () => void
) => { ) => {
const onSubmit = useCallback(console.log, []); const dispatch = useDispatch();
const onSubmit = useCallback((values: INode, helpers: FormikHelpers<INode>) => {
helpers.setSubmitting(true);
dispatch(nodeSubmitLocal(values, onSuccess(helpers)));
}, []);
const { current: initialValues } = useRef(values); const { current: initialValues } = useRef(values);
const onReset = useCallback(() => { const onReset = useCallback(() => {