add actions

This commit is contained in:
2021-07-17 18:43:21 +03:00
parent 1d228b0669
commit 91b5ddcfaf
16 changed files with 475 additions and 25 deletions

View File

@ -9,3 +9,4 @@ webpack.config.js
jest.config.js jest.config.js
babel.config.js babel.config.js
/scripts /scripts
server.js

View File

@ -0,0 +1,266 @@
import {Atom} from '@reatom/core';
import {useAtom} from '@reatom/react';
import {Button, Checkbox as CheckboxInput, Drawer, Input, Select as SelectInput} from 'antd';
import {CheckboxChangeEvent} from 'antd/lib/checkbox';
import {SelectValue} from 'antd/lib/select';
import React, {FC, Fragment, memo, SyntheticEvent, useCallback, useEffect, useMemo} from 'react';
import {createUseStyles} from 'react-jss';
import {queryParsers} from '../../../pages/users/utils';
import {useQuery} from '../../hooks/useQuery';
import {CrudService} from '../../services/CrudService';
import {EntityMode} from '../../types/EntityModes';
export enum FormInputType {
Text = 'text',
Checkbox = 'checkobx',
Select = 'select',
}
type SelectOption = {
value: string;
label: string;
};
type FormOption = {
name: string;
label: string;
type?: FormInputType;
options?: SelectOption[];
checkboxLabel?: string;
};
type Options<T> = {
entityFormAtom: Atom<T>;
service: CrudService<T>;
formOptions: FormOption[];
entityName: string;
};
const AVAILABLE_CLOSE_MODES = [EntityMode.Show];
const DISABLED_FORM_MODES = [EntityMode.Show];
const useStyles = createUseStyles({
button: {
marginRight: '8px',
},
input: {
marginBottom: '16px',
width: '100%',
},
});
export const createEntitySidebar = function <T> ({
entityFormAtom,
service,
formOptions,
entityName,
}: Options<T>): FC {
return memo(() => {
const {mode, id} = useQuery(queryParsers);
const form = useAtom(entityFormAtom);
const classes = useStyles();
useEffect(() => {
service.loadEntity(id);
}, [id, mode]);
const handleClose = () => {
service.navigate();
};
const onChangeInput = (event: SyntheticEvent<HTMLInputElement>) => {
const {name, value} = event.currentTarget;
service.loadEntityForm({
...form,
[name]: value,
});
};
const onChangeCheckbox = (event: CheckboxChangeEvent) => {
const {name, checked} = event.target;
service.loadEntityForm({
...form,
[name!]: checked,
});
};
const onChangeSelect = (name: string) => (value: SelectValue) => {
if (!Array.isArray(value)) {
service.loadEntityForm({
...form,
[name]: value,
});
}
};
const disabled = useMemo(() => !mode || DISABLED_FORM_MODES.includes(mode), [mode]);
const handleCreateUser = useCallback(() => {
service.createEntity(form);
}, [form]);
const handleSaveUser = useCallback(() => {
if (id) {
service.updateEntity({
...form,
id,
});
}
}, [form, id]);
const handleCopy = useCallback(() => {
service.navigate(EntityMode.Copy, id);
}, [id]);
const handleEdit = useCallback(() => {
service.navigate(EntityMode.Edit, id);
}, [id]);
const handleDelete = useCallback(() => {
service.removeEntity(id);
}, [id]);
const handleBackdrop = useCallback(() => {
if (mode && AVAILABLE_CLOSE_MODES.includes(mode)) {
handleClose();
}
}, [mode]);
const title = useMemo(() => {
switch (mode) {
case EntityMode.Create:
return `Creating a ${entityName}`;
case EntityMode.Copy:
return `Coping ${entityName} "${id}"`;
case EntityMode.Edit:
return `Editing ${entityName} "${id}"`;
case EntityMode.Show:
return `Viewing ${entityName} "${id}"`;
default:
return `Mode "${mode}" not supported for ${entityName} form`;
}
}, [mode, id]);
const primaryButton = useMemo(() => {
switch (mode) {
case EntityMode.Create:
case EntityMode.Copy:
return (
<Button
className={classes.button}
onClick={handleCreateUser}
type="primary"
>
Create
</Button>
);
case EntityMode.Edit:
return (
<Button
className={classes.button}
onClick={handleSaveUser}
type="primary"
>
Save
</Button>
);
case EntityMode.Show:
return (
<Button
className={classes.button}
onClick={handleEdit}
type="primary"
>
Edit
</Button>
);
default:
return null;
}
}, [mode, classes, handleEdit, handleCreateUser, handleSaveUser]);
const renderFooter = useMemo(() => {
return (
<div>
{primaryButton}
{mode === EntityMode.Show && (
<Fragment>
<Button
className={classes.button}
onClick={handleCopy}
>
Copy
</Button>
<Button
className={classes.button}
onClick={handleDelete}
>
Delete
</Button>
</Fragment>
)}
<Button onClick={handleClose}>Cancel</Button>
</div>
);
}, [primaryButton, mode, classes, handleCopy, handleDelete]);
return (
<Drawer
visible={!!mode}
closable={false}
onClose={handleBackdrop}
width="600"
title={title}
footer={renderFooter}
>
<form>
{formOptions.map(({name, label, type = FormInputType.Text, options, checkboxLabel}) => {
return (
<div key={name}>
<label>{label}:</label>
{type === FormInputType.Checkbox && (
<CheckboxInput
name={name}
className={classes.input}
disabled={disabled}
value={(form as any)[name]}
onChange={onChangeCheckbox}
type="checkbox"
>
{checkboxLabel ?? label}
</CheckboxInput>
)}
{type === FormInputType.Select && (
<SelectInput
className={classes.input}
disabled={disabled}
value={(form as any)[name]}
onChange={onChangeSelect(name)}
>
{(options ?? []).map(option => (
<SelectInput.Option
value={option.value}
key={option.value}
>
{option.label}
</SelectInput.Option>
))}
</SelectInput>
)}
{type === FormInputType.Text && (
<Input
name={name}
className={classes.input}
disabled={disabled}
value={(form as any)[name]}
onChange={onChangeInput}
/>
)}
</div>
);
})}
</form>
</Drawer>
);
});
};

View File

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

View File

@ -7,15 +7,15 @@ import {CrudService} from '../../services/CrudService';
import {EntityMode} from '../../types/EntityModes'; import {EntityMode} from '../../types/EntityModes';
import {EntityWithId} from '../../api/CrudAPI'; import {EntityWithId} from '../../api/CrudAPI';
type Props<T> = { type Options<T> = {
entityListAtom: Atom<T[]>; entityListAtom: Atom<T[]>;
service: CrudService<T>; service: CrudService<T>;
}; };
export const createEntityTable = function <T extends EntityWithId<unknown>> ({ export const createEntityTable = function <T> ({
entityListAtom, entityListAtom,
service, service,
}: Props<T>): FC { }: Options<T>): FC {
return memo(() => { return memo(() => {
const entityList = useAtom(entityListAtom); const entityList = useAtom(entityListAtom);
@ -44,7 +44,7 @@ export const createEntityTable = function <T extends EntityWithId<unknown>> ({
}, [entityList]); }, [entityList]);
const dataSource = useMemo(() => { const dataSource = useMemo(() => {
return entityList.map(entity => { return entityList.map((entity: any) => {
return { return {
...entity, ...entity,
key: entity.id, key: entity.id,

View File

@ -7,11 +7,13 @@ export const ROUTES = {
CURRENCIES: '/currencies', CURRENCIES: '/currencies',
}; };
const PROTOCOL = location.protocol;
export const ENDPOINT = { export const ENDPOINT = {
AUTH: 'https://localhost:3189/api/auth', AUTH: `${PROTOCOL}//localhost:3189/api/auth`,
USERS: 'https://localhost:3189/api/users', USERS: `${PROTOCOL}//localhost:3189/api/users`,
ACTIONS: 'https://localhost:3189/api/bot/actions', ACTIONS: `${PROTOCOL}//localhost:3189/api/bot/actions`,
CONDITIONS: 'https://localhost:3189/api/bot/conditions', CONDITIONS: `${PROTOCOL}//localhost:3189/api/bot/conditions`,
GRAPHS: 'https://localhost:3189/api/bot/graphs', GRAPHS: `${PROTOCOL}//localhost:3189/api/bot/graphs`,
CURRENCIES: 'https://localhost:3189/api/bot/currencies', CURRENCIES: `${PROTOCOL}//localhost:3189/api/bot/currencies`,
}; };

View File

@ -1,9 +1,8 @@
import {declareAction, declareAtom} from '@reatom/core'; import {declareAction, declareAtom} from '@reatom/core';
import {EntityWithId} from '../../api/CrudAPI';
import {store} from './store'; import {store} from './store';
export const createEntityAtoms = <T>(initEntity: T) => { export const createEntityAtoms = <T>(initEntity: T) => {
const INIT_ENTITY_LIST: EntityWithId<T>[] = []; const INIT_ENTITY_LIST: T[] = [];
const loadEntityList = declareAction<typeof INIT_ENTITY_LIST>(); const loadEntityList = declareAction<typeof INIT_ENTITY_LIST>();
const loadEntityForm = declareAction<T>(); const loadEntityForm = declareAction<T>();

View File

@ -20,6 +20,10 @@ export class CrudService<T> {
this.route = route; this.route = route;
} }
loadEntityForm(form: T) {
this.actions.loadEntityForm(form);
}
loadEntityList() { loadEntityList() {
return this.api return this.api
.request() .request()

View File

@ -1,4 +0,0 @@
import {ENDPOINT} from '_consts/common';
import {CrudAPI} from '../../../core/api/CrudAPI';
export const actionsAPI = new CrudAPI(ENDPOINT.ACTIONS);

View File

@ -1,11 +1,74 @@
import {Button, Layout} from 'antd';
import moment from 'moment';
import React, {FC, memo} from 'react'; import React, {FC, memo} from 'react';
import {actionsAPI} from '../../api/ActionsAPI'; import {createUseStyles} from 'react-jss';
import {createEntityAtoms} from '_infrastructure/atom/createEntityAtoms';
import {createEntitySidebar, FormInputType} from '../../../../core/blocks/entity-sidebar';
import {createEntityTable} from '../../../../core/blocks/entity-table';
import {ENDPOINT, ROUTES} from '../../../../core/consts/common';
import {CrudService} from '../../../../core/services/CrudService';
import {EntityMode} from '../../../../core/types/EntityModes';
import {ActionModel} from '../../types';
actionsAPI.request(); const {entityListAtom, entityFormAtom, bindedActions} = createEntityAtoms<ActionModel>({
createAt: moment().toISOString(),
closedAt: moment().toISOString(),
type: '',
login: '',
isExperiment: true,
});
const service = new CrudService<ActionModel>(ROUTES.ACTIONS, ENDPOINT.ACTIONS, bindedActions);
const TYPE_SELECT_OPTIONS = [
{value: 'up', label: 'Up'},
{value: 'down', label: 'Down'},
];
const formOptions = [
{name: 'createAt', label: 'Create at'},
{name: 'closedAt', label: 'Close at'},
{name: 'type', label: 'Type', type: FormInputType.Select, options: TYPE_SELECT_OPTIONS},
{name: 'login', label: 'Login'},
{name: 'isExperiment', label: 'Is experiment', type: FormInputType.Checkbox, checkboxLabel: 'Enabled experiment'},
];
const EntityTable = createEntityTable({entityListAtom, service});
const EntitySidebar = createEntitySidebar({
entityFormAtom,
service,
formOptions,
entityName: 'Action'
});
const useStyles = createUseStyles({
header: {
backgroundColor: '#fff',
}
});
const handleClickNewEntity = () => {
service.navigate(EntityMode.Create);
};
const Page: FC = () => { const Page: FC = () => {
const classes = useStyles();
return ( return (
<div>actions</div> <Layout>
<Layout.Header className={classes.header}>
<Button
type="primary"
onClick={handleClickNewEntity}
>
New action
</Button>
</Layout.Header>
<Layout.Content>
<EntityTable />
<EntitySidebar />
</Layout.Content>
</Layout>
); );
}; };

View File

@ -0,0 +1,7 @@
export type ActionModel = {
createAt: string;
closedAt: string;
type: string;
login: string;
isExperiment: boolean;
};

View File

@ -1,8 +1,55 @@
import {Button, Layout} from 'antd';
import React, {FC, memo} from 'react'; import React, {FC, memo} from 'react';
import {createUseStyles} from 'react-jss';
import {createEntitySidebar} from '../../../../core/blocks/entity-sidebar';
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 {CurrencyModel} from '../../types';
const {entityListAtom, entityFormAtom, bindedActions} = createEntityAtoms<CurrencyModel>({
name: '',
});
const service = new CrudService<CurrencyModel>(ROUTES.GRAPHS, ENDPOINT.GRAPHS, bindedActions);
const formOptions = [
{name: 'name', label: 'Name'},
];
const EntityTable = createEntityTable({entityListAtom, service});
const EntitySidebar = createEntitySidebar({entityFormAtom, service, formOptions, entityName: 'Currency'});
const useStyles = createUseStyles({
header: {
backgroundColor: '#fff',
}
});
const handleClickNewEntity = () => {
service.navigate(EntityMode.Create);
};
const Page: FC = () => { const Page: FC = () => {
const classes = useStyles();
return ( return (
<div>conditions</div> <Layout>
<Layout.Header className={classes.header}>
<Button
type="primary"
onClick={handleClickNewEntity}
>
New currency
</Button>
</Layout.Header>
<Layout.Content>
<EntityTable />
<EntitySidebar />
</Layout.Content>
</Layout>
); );
}; };

View File

@ -0,0 +1,3 @@
export type CurrencyModel = {
name: string;
};

View File

@ -1,8 +1,55 @@
import {Button, Layout} from 'antd';
import React, {FC, memo} from 'react'; import React, {FC, memo} from 'react';
import {createUseStyles} from 'react-jss';
import {createEntitySidebar} from '../../../../core/blocks/entity-sidebar';
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 {CurrencyModel} from '../../types';
const {entityListAtom, entityFormAtom, bindedActions} = createEntityAtoms<CurrencyModel>({
name: '',
});
const service = new CrudService<CurrencyModel>(ROUTES.CURRENCIES, ENDPOINT.CURRENCIES, bindedActions);
const formOptions = [
{name: 'name', label: 'Name'},
];
const EntityTable = createEntityTable({entityListAtom, service});
const EntitySidebar = createEntitySidebar({entityFormAtom, service, formOptions, entityName: 'Currency'});
const useStyles = createUseStyles({
header: {
backgroundColor: '#fff',
}
});
const handleClickNewEntity = () => {
service.navigate(EntityMode.Create);
};
const Page: FC = () => { const Page: FC = () => {
const classes = useStyles();
return ( return (
<div>currencies</div> <Layout>
<Layout.Header className={classes.header}>
<Button
type="primary"
onClick={handleClickNewEntity}
>
New currency
</Button>
</Layout.Header>
<Layout.Content>
<EntityTable />
<EntitySidebar />
</Layout.Content>
</Layout>
); );
}; };

View File

@ -0,0 +1,5 @@
type Currency = {
name: string;
};
export type CurrencyModel = Record<keyof Currency, string>;

View File

@ -1,6 +1,7 @@
import {Button, Layout} from 'antd'; import {Button, Layout} from 'antd';
import React, {FC, memo} from 'react'; import React, {FC, memo} from 'react';
import {createUseStyles} from 'react-jss'; import {createUseStyles} from 'react-jss';
import {createEntitySidebar} from '../../../../core/blocks/entity-sidebar';
import {createEntityTable} from '../../../../core/blocks/entity-table'; import {createEntityTable} from '../../../../core/blocks/entity-table';
import {ENDPOINT, ROUTES} from '../../../../core/consts/common'; import {ENDPOINT, ROUTES} from '../../../../core/consts/common';
import {createEntityAtoms} from '../../../../core/infrastructure/atom/createEntityAtoms'; import {createEntityAtoms} from '../../../../core/infrastructure/atom/createEntityAtoms';
@ -8,16 +9,24 @@ import {CrudService} from '../../../../core/services/CrudService';
import {EntityMode} from '../../../../core/types/EntityModes'; import {EntityMode} from '../../../../core/types/EntityModes';
import {GraphModel} from '../../types'; import {GraphModel} from '../../types';
const {entityListAtom, bindedActions} = createEntityAtoms<GraphModel>({ const {entityListAtom, entityFormAtom, bindedActions} = createEntityAtoms<GraphModel>({
type: '', type: '',
graphName: '', graphName: '',
from: '', from: '',
to: '', to: '',
}); });
const service = new CrudService(ROUTES.GRAPHS, ENDPOINT.GRAPHS, bindedActions); const service = new CrudService<GraphModel>(ROUTES.GRAPHS, ENDPOINT.GRAPHS, bindedActions);
const formOptions = [
{name: 'type', label: 'Type'},
{name: 'graphName', label: 'Graph name'},
{name: 'from', label: 'From'},
{name: 'to', label: 'To'},
];
const EntityTable = createEntityTable({entityListAtom, service}); const EntityTable = createEntityTable({entityListAtom, service});
const EntitySidebar = createEntitySidebar({entityFormAtom, service, formOptions, entityName: 'Graph'});
const useStyles = createUseStyles({ const useStyles = createUseStyles({
header: { header: {
@ -44,6 +53,7 @@ const Page: FC = () => {
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<EntityTable /> <EntityTable />
<EntitySidebar />
</Layout.Content> </Layout.Content>
</Layout> </Layout>
); );

View File

@ -26,7 +26,6 @@ module.exports = {
compress: true, compress: true,
open: true, open: true,
port: 3189, port: 3189,
http2: true,
proxy: { proxy: {
'/api/users': { '/api/users': {
target: 'http://vigdorov.ru:3011', target: 'http://vigdorov.ru:3011',