add actions
This commit is contained in:
@ -9,3 +9,4 @@ webpack.config.js
|
||||
jest.config.js
|
||||
babel.config.js
|
||||
/scripts
|
||||
server.js
|
||||
266
src/core/blocks/entity-sidebar/EntitySidebar.tsx
Normal file
266
src/core/blocks/entity-sidebar/EntitySidebar.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
};
|
||||
1
src/core/blocks/entity-sidebar/index.ts
Normal file
1
src/core/blocks/entity-sidebar/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './EntitySidebar';
|
||||
@ -7,15 +7,15 @@ import {CrudService} from '../../services/CrudService';
|
||||
import {EntityMode} from '../../types/EntityModes';
|
||||
import {EntityWithId} from '../../api/CrudAPI';
|
||||
|
||||
type Props<T> = {
|
||||
type Options<T> = {
|
||||
entityListAtom: Atom<T[]>;
|
||||
service: CrudService<T>;
|
||||
};
|
||||
|
||||
export const createEntityTable = function <T extends EntityWithId<unknown>> ({
|
||||
export const createEntityTable = function <T> ({
|
||||
entityListAtom,
|
||||
service,
|
||||
}: Props<T>): FC {
|
||||
}: Options<T>): FC {
|
||||
return memo(() => {
|
||||
const entityList = useAtom(entityListAtom);
|
||||
|
||||
@ -44,7 +44,7 @@ export const createEntityTable = function <T extends EntityWithId<unknown>> ({
|
||||
}, [entityList]);
|
||||
|
||||
const dataSource = useMemo(() => {
|
||||
return entityList.map(entity => {
|
||||
return entityList.map((entity: any) => {
|
||||
return {
|
||||
...entity,
|
||||
key: entity.id,
|
||||
|
||||
@ -7,11 +7,13 @@ export const ROUTES = {
|
||||
CURRENCIES: '/currencies',
|
||||
};
|
||||
|
||||
const PROTOCOL = location.protocol;
|
||||
|
||||
export const ENDPOINT = {
|
||||
AUTH: 'https://localhost:3189/api/auth',
|
||||
USERS: 'https://localhost:3189/api/users',
|
||||
ACTIONS: 'https://localhost:3189/api/bot/actions',
|
||||
CONDITIONS: 'https://localhost:3189/api/bot/conditions',
|
||||
GRAPHS: 'https://localhost:3189/api/bot/graphs',
|
||||
CURRENCIES: 'https://localhost:3189/api/bot/currencies',
|
||||
AUTH: `${PROTOCOL}//localhost:3189/api/auth`,
|
||||
USERS: `${PROTOCOL}//localhost:3189/api/users`,
|
||||
ACTIONS: `${PROTOCOL}//localhost:3189/api/bot/actions`,
|
||||
CONDITIONS: `${PROTOCOL}//localhost:3189/api/bot/conditions`,
|
||||
GRAPHS: `${PROTOCOL}//localhost:3189/api/bot/graphs`,
|
||||
CURRENCIES: `${PROTOCOL}//localhost:3189/api/bot/currencies`,
|
||||
};
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
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 INIT_ENTITY_LIST: T[] = [];
|
||||
|
||||
const loadEntityList = declareAction<typeof INIT_ENTITY_LIST>();
|
||||
const loadEntityForm = declareAction<T>();
|
||||
|
||||
@ -20,6 +20,10 @@ export class CrudService<T> {
|
||||
this.route = route;
|
||||
}
|
||||
|
||||
loadEntityForm(form: T) {
|
||||
this.actions.loadEntityForm(form);
|
||||
}
|
||||
|
||||
loadEntityList() {
|
||||
return this.api
|
||||
.request()
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
import {ENDPOINT} from '_consts/common';
|
||||
import {CrudAPI} from '../../../core/api/CrudAPI';
|
||||
|
||||
export const actionsAPI = new CrudAPI(ENDPOINT.ACTIONS);
|
||||
@ -1,11 +1,74 @@
|
||||
import {Button, Layout} from 'antd';
|
||||
import moment from 'moment';
|
||||
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 classes = useStyles();
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
7
src/pages/actions/types.ts
Normal file
7
src/pages/actions/types.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export type ActionModel = {
|
||||
createAt: string;
|
||||
closedAt: string;
|
||||
type: string;
|
||||
login: string;
|
||||
isExperiment: boolean;
|
||||
};
|
||||
@ -1,8 +1,55 @@
|
||||
import {Button, Layout} from 'antd';
|
||||
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 classes = useStyles();
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
3
src/pages/conditions/types.ts
Normal file
3
src/pages/conditions/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export type CurrencyModel = {
|
||||
name: string;
|
||||
};
|
||||
@ -1,8 +1,55 @@
|
||||
import {Button, Layout} from 'antd';
|
||||
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 classes = useStyles();
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
5
src/pages/currencies/types.ts
Normal file
5
src/pages/currencies/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
type Currency = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type CurrencyModel = Record<keyof Currency, string>;
|
||||
@ -1,6 +1,7 @@
|
||||
import {Button, Layout} from 'antd';
|
||||
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';
|
||||
@ -8,16 +9,24 @@ import {CrudService} from '../../../../core/services/CrudService';
|
||||
import {EntityMode} from '../../../../core/types/EntityModes';
|
||||
import {GraphModel} from '../../types';
|
||||
|
||||
const {entityListAtom, bindedActions} = createEntityAtoms<GraphModel>({
|
||||
const {entityListAtom, entityFormAtom, bindedActions} = createEntityAtoms<GraphModel>({
|
||||
type: '',
|
||||
graphName: '',
|
||||
from: '',
|
||||
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 EntitySidebar = createEntitySidebar({entityFormAtom, service, formOptions, entityName: 'Graph'});
|
||||
|
||||
const useStyles = createUseStyles({
|
||||
header: {
|
||||
@ -44,6 +53,7 @@ const Page: FC = () => {
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<EntityTable />
|
||||
<EntitySidebar />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@ -26,7 +26,6 @@ module.exports = {
|
||||
compress: true,
|
||||
open: true,
|
||||
port: 3189,
|
||||
http2: true,
|
||||
proxy: {
|
||||
'/api/users': {
|
||||
target: 'http://vigdorov.ru:3011',
|
||||
|
||||
Reference in New Issue
Block a user