add crud service, crud atoms, crud table, graphs page

This commit is contained in:
2021-06-26 00:28:39 +03:00
parent 40ac760857
commit 1d228b0669
18 changed files with 267 additions and 59 deletions

View File

@ -60,8 +60,8 @@ export class CrudAPI<T> {
return http.get<never, ResponseEntities<T>>([this.endpoint, query.join('&')].filter(isNotEmpty).join('?')); return http.get<never, ResponseEntities<T>>([this.endpoint, query.join('&')].filter(isNotEmpty).join('?'));
} }
find = (id: string): Promise<T> => { find = (id: string): Promise<EntityWithId<T>> => {
return http.get<never, T>(`${this.endpoint}/${id}`); return http.get<never, EntityWithId<T>>(`${this.endpoint}/${id}`);
} }
create = (entity: EntityWithoutId<T>): Promise<EntityWithId<T>> => { create = (entity: EntityWithoutId<T>): Promise<EntityWithId<T>> => {

View File

@ -0,0 +1,64 @@
import {Atom} from '@reatom/core';
import {useAtom} from '@reatom/react';
import {head} from 'lodash';
import Table, {ColumnsType} from 'antd/lib/table';
import React, {FC, memo, useCallback, useEffect, useMemo} from 'react';
import {CrudService} from '../../services/CrudService';
import {EntityMode} from '../../types/EntityModes';
import {EntityWithId} from '../../api/CrudAPI';
type Props<T> = {
entityListAtom: Atom<T[]>;
service: CrudService<T>;
};
export const createEntityTable = function <T extends EntityWithId<unknown>> ({
entityListAtom,
service,
}: Props<T>): FC {
return memo(() => {
const entityList = useAtom(entityListAtom);
useEffect(() => {
service.loadEntityList();
}, []);
const onRow = useCallback((entity: EntityWithId<T>) => ({
onClick: () => {
service.navigate(EntityMode.Show, entity.id);
},
}), []);
const columns: ColumnsType<EntityWithId<T>> = useMemo(() => {
const entity = head(entityList);
if (entity) {
return Object.keys(entity).map(field => {
return {
title: field,
dataIndex: field,
key: field,
};
});
}
return [];
}, [entityList]);
const dataSource = useMemo(() => {
return entityList.map(entity => {
return {
...entity,
key: entity.id,
};
});
}, [entityList]);
return (
<Table
columns={columns}
dataSource={dataSource}
onRow={onRow}
/>
);
});
};

View File

@ -0,0 +1 @@
export * from './EntityTable';

View File

@ -1,3 +1,5 @@
type Undefinable<T> = T | undefined; type Undefinable<T> = T | undefined;
type Nullable<T> = T | undefined | null; type Nullable<T> = T | undefined | null;
type Indexed<T> = Record<string, T>;

View File

@ -0,0 +1,28 @@
import {declareAction, declareAtom} from '@reatom/core';
import {EntityWithId} from '../../api/CrudAPI';
import {store} from './store';
export const createEntityAtoms = <T>(initEntity: T) => {
const INIT_ENTITY_LIST: EntityWithId<T>[] = [];
const loadEntityList = declareAction<typeof INIT_ENTITY_LIST>();
const loadEntityForm = declareAction<T>();
const entityListAtom = declareAtom(INIT_ENTITY_LIST, on => [
on(loadEntityList, (_state, payload) => payload)
]);
const entityFormAtom = declareAtom(initEntity, on => [
on(loadEntityForm, (_state, payload) => payload)
]);
const bindedActions = {
loadEntityList: (entities: typeof INIT_ENTITY_LIST) => {
store.dispatch(loadEntityList(entities));
},
loadEntityForm: (entity: T) => {
store.dispatch(loadEntityForm(entity));
},
};
return {entityListAtom, entityFormAtom, bindedActions};
};

View File

@ -2,14 +2,6 @@ import {declareAction, declareAtom} from '@reatom/core';
import {User} from '../../../pages/users/types'; import {User} from '../../../pages/users/types';
import {store} from './store'; import {store} from './store';
export type FieldData = {
name: keyof User | Array<keyof User>;
value?: any;
touched?: boolean;
validating?: boolean;
errors?: string[];
};
const INIT_USERS: User[] = []; const INIT_USERS: User[] = [];
export const INIT_USER: User = { export const INIT_USER: User = {
id: '', id: '',

View File

@ -0,0 +1,70 @@
import {CrudAPI, EntityWithId, EntityWithoutId} from '../api/CrudAPI';
import {EntityMode} from '../types/EntityModes';
import {routerService} from './RouterService';
type Actions<T> = {
loadEntityList: (entities: T[]) => void,
loadEntityForm: (entity: T) => void;
};
export class CrudService<T> {
api: CrudAPI<T>;
actions: Actions<T>;
route: string;
constructor(route: string, endpoint: string, actions: Actions<T>) {
this.api = new CrudAPI(endpoint);
this.actions = actions;
this.route = route;
}
loadEntityList() {
return this.api
.request()
.then(({data}) => {
this.actions.loadEntityList(data);
});
}
loadEntity(id?: string) {
if (id) {
this.api
.find(id)
.then(entity => {
this.actions.loadEntityForm(entity);
});
}
}
updateEntity({id, ...entity}: EntityWithId<T>) {
this.api
.update(id, entity)
.then(this.goRootAndReload);
}
removeEntity(id?: string) {
if (id) {
this.api
.remove(id)
.then(this.goRootAndReload);
}
}
createEntity(entity: EntityWithoutId<T>) {
this.api
.create(entity)
.then(this.goRootAndReload);
}
goRootAndReload = () => {
this.loadEntityList().then(() => {
this.navigate();
});
}
navigate(mode?: EntityMode, id?: string) {
routerService.pushWithQuery(this.route, {mode, id}, {reset: true});
}
}

View File

@ -1,6 +1,7 @@
import {decode, encode, ParsedUrlQuery} from 'querystring'; import {decode, encode, ParsedUrlQuery} from 'querystring';
import {bindedActions} from '../infrastructure/atom/routerAtom'; import {bindedActions} from '../infrastructure/atom/routerAtom';
import {isNotEmpty} from '../referers/common'; import {isNotEmpty} from '../referers/common';
import {objectEntries} from '../utils/objectEntries';
type PushQueryOptions = { type PushQueryOptions = {
reset?: boolean; reset?: boolean;
@ -14,7 +15,7 @@ class RouterService {
bindedActions.routerAction(route); bindedActions.routerAction(route);
} }
pushWithQuery(path: string, query: ParsedUrlQuery, options?: PushQueryOptions) { pushWithQuery(path: string, query: Indexed<Undefinable<string | string[]>>, options?: PushQueryOptions) {
const currentQuery = getQuery(); const currentQuery = getQuery();
const finalQuery = encode({ const finalQuery = encode({
@ -24,7 +25,12 @@ class RouterService {
...(options?.shouldRefresh ? { ...(options?.shouldRefresh ? {
__timestamp: Date.now(), __timestamp: Date.now(),
} : {}), } : {}),
...query, ...objectEntries(query).reduce<ParsedUrlQuery>((acc, [key, value]) => {
if (value) {
acc[key] = value;
}
return acc;
}, {}),
}); });
this.push([path, finalQuery].filter(isNotEmpty).join('?')); this.push([path, finalQuery].filter(isNotEmpty).join('?'));

View File

@ -1,8 +1,51 @@
import {Button, Layout} from 'antd';
import React, {FC, memo} from 'react'; import React, {FC, memo} from 'react';
import {createUseStyles} from 'react-jss';
import {createEntityTable} from '../../../../core/blocks/entity-table';
import {ENDPOINT, ROUTES} from '../../../../core/consts/common';
import {createEntityAtoms} from '../../../../core/infrastructure/atom/createEntityAtoms';
import {CrudService} from '../../../../core/services/CrudService';
import {EntityMode} from '../../../../core/types/EntityModes';
import {GraphModel} from '../../types';
const {entityListAtom, bindedActions} = createEntityAtoms<GraphModel>({
type: '',
graphName: '',
from: '',
to: '',
});
const service = new CrudService(ROUTES.GRAPHS, ENDPOINT.GRAPHS, bindedActions);
const EntityTable = createEntityTable({entityListAtom, service});
const useStyles = createUseStyles({
header: {
backgroundColor: '#fff',
}
});
const handleClickNewEntity = () => {
service.navigate(EntityMode.Create);
};
const Page: FC = () => { const Page: FC = () => {
const classes = useStyles();
return ( return (
<div>graphs</div> <Layout>
<Layout.Header className={classes.header}>
<Button
type="primary"
onClick={handleClickNewEntity}
>
New graph
</Button>
</Layout.Header>
<Layout.Content>
<EntityTable />
</Layout.Content>
</Layout>
); );
}; };

View File

@ -0,0 +1,8 @@
type Graph = {
type: string;
graphName: string;
from: number;
to: number;
};
export type GraphModel = Record<keyof Graph, string>;

View File

@ -1,6 +1,6 @@
import {ENDPOINT} from '_consts/common'; import {ENDPOINT} from '_consts/common';
import {CrudAPI, EntityWithId, EntityWithoutId} from '../../../core/api/CrudAPI'; import {CrudAPI, EntityWithId, EntityWithoutId} from '_api/CrudAPI';
import {http} from '../../../core/infrastructure/Http'; import {http} from '_infrastructure/Http';
import {User} from '../types'; import {User} from '../types';
import {ChangePasswordRequest} from './types'; import {ChangePasswordRequest} from './types';

View File

@ -1,8 +1,8 @@
import {Input, Modal} from 'antd'; import {Input, Modal} from 'antd';
import React, {FC, memo, useCallback, useState} from 'react'; import React, {FC, memo, useCallback, useState} from 'react';
import {ROUTES} from '../../../../core/consts/common'; import {ROUTES} from '_consts/common';
import {useQuery} from '../../../../core/hooks/useQuery'; import {useQuery} from '_hooks/useQuery';
import {routerService} from '../../../../core/services/RouterService'; import {routerService} from '_services/RouterService';
import {LABELS} from '../../consts'; import {LABELS} from '../../consts';
import {ModalType} from '../../enums'; import {ModalType} from '../../enums';
import {usersService} from '../../services/UsersServices'; import {usersService} from '../../services/UsersServices';

View File

@ -1,9 +1,9 @@
import {Button, Layout} from 'antd'; import {Button, Layout} from 'antd';
import React, {FC, Fragment, memo} from 'react'; import React, {FC, Fragment, memo} from 'react';
import {createUseStyles} from 'react-jss'; import {createUseStyles} from 'react-jss';
import {ROUTES} from '../../../../core/consts/common'; import {ROUTES} from '_consts/common';
import {routerService} from '../../../../core/services/RouterService'; import {routerService} from '_services/RouterService';
import {EntityMode} from '../../../../core/types/EntityModes'; import {EntityMode} from '_types/EntityModes';
import ChangePasswordModal from '../change-password-modal/ChangePasswordModal'; import ChangePasswordModal from '../change-password-modal/ChangePasswordModal';
import UserSidebar from '../user-sidebar/UserSidebar'; import UserSidebar from '../user-sidebar/UserSidebar';
import UsersTable from '../users-table/UsersTable'; import UsersTable from '../users-table/UsersTable';

View File

@ -2,11 +2,11 @@ import {useAtom} from '@reatom/react';
import {Button, Drawer, Input} from 'antd'; import {Button, Drawer, Input} from 'antd';
import React, {FC, Fragment, memo, SyntheticEvent, useCallback, useEffect, useMemo} from 'react'; import React, {FC, Fragment, memo, SyntheticEvent, useCallback, useEffect, useMemo} from 'react';
import {createUseStyles} from 'react-jss'; import {createUseStyles} from 'react-jss';
import {ROUTES} from '../../../../core/consts/common'; import {ROUTES} from '_consts/common';
import {useQuery} from '../../../../core/hooks/useQuery'; import {useQuery} from '_hooks/useQuery';
import {bindedActions, userFormAtom} from '../../../../core/infrastructure/atom/usersAtom'; import {bindedActions, userFormAtom} from '_infrastructure/atom/usersAtom';
import {routerService} from '../../../../core/services/RouterService'; import {routerService} from '_services/RouterService';
import {EntityMode} from '../../../../core/types/EntityModes'; import {EntityMode} from '_types/EntityModes';
import {usersService} from '../../services/UsersServices'; import {usersService} from '../../services/UsersServices';
import {queryParsers} from '../../utils'; import {queryParsers} from '../../utils';

View File

@ -3,11 +3,11 @@ import {Button, Table} from 'antd';
import {ColumnsType} from 'antd/lib/table'; import {ColumnsType} from 'antd/lib/table';
import {head} from 'lodash'; import {head} from 'lodash';
import React, {FC, memo, useEffect, useMemo} from 'react'; import React, {FC, memo, useEffect, useMemo} from 'react';
import {ROUTES} from '../../../../core/consts/common'; import {ROUTES} from '_consts/common';
import {usersAtom} from '../../../../core/infrastructure/atom/usersAtom'; import {usersAtom} from '_infrastructure/atom/usersAtom';
import {routerService} from '../../../../core/services/RouterService'; import {routerService} from '_services/RouterService';
import {EntityMode} from '../../../../core/types/EntityModes'; import {EntityMode} from '_types/EntityModes';
import {objectKeys} from '../../../../core/utils/objectKeys'; import {objectKeys} from '_utils/objectKeys';
import {ModalType} from '../../enums'; import {ModalType} from '../../enums';
import {usersService} from '../../services/UsersServices'; import {usersService} from '../../services/UsersServices';
import {User} from '../../types'; import {User} from '../../types';
@ -37,7 +37,8 @@ const UsersTable: FC = () => {
}, []); }, []);
const columns: ColumnsType<User> = useMemo(() => { const columns: ColumnsType<User> = useMemo(() => {
const user = head(users) ?? {} as User; const user = head(users);
if (user) {
return objectKeys(user).map(field => { return objectKeys(user).map(field => {
if (field === 'password') { if (field === 'password') {
return { return {
@ -54,6 +55,8 @@ const UsersTable: FC = () => {
key: field, key: field,
}; };
}); });
}
return [];
}, [users]); }, [users]);
const dataSource = useMemo(() => { const dataSource = useMemo(() => {

View File

@ -1,7 +1,7 @@
import {EntityWithoutId} from '../../../core/api/CrudAPI'; import {EntityWithoutId} from '_api/CrudAPI';
import {ROUTES} from '../../../core/consts/common'; import {ROUTES} from '_consts/common';
import {bindedActions, INIT_USER} from '../../../core/infrastructure/atom/usersAtom'; import {bindedActions, INIT_USER} from '_infrastructure/atom/usersAtom';
import {routerService} from '../../../core/services/RouterService'; import {routerService} from '_services/RouterService';
import {usersAPI} from '../api/UsersAPI'; import {usersAPI} from '../api/UsersAPI';
import {User} from '../types'; import {User} from '../types';

View File

@ -1,14 +1,5 @@
import {EntityMode} from '../../core/types/EntityModes';
import {ModalType} from './enums';
export type User = { export type User = {
id: string; id: string;
login: string; login: string;
password: string; password: string;
}; };
export type QueryParams = {
id?: string;
mode?: EntityMode;
modalType?: ModalType;
};

View File

@ -1,5 +1,5 @@
import {EntityMode} from '../../core/types/EntityModes'; import {EntityMode} from '_types/EntityModes';
import {stringParser} from '../../core/utils/queryParsers'; import {stringParser} from '_utils/queryParsers';
import {ModalType} from './enums'; import {ModalType} from './enums';
export const queryParsers = { export const queryParsers = {