node-red-contrib-chatbot
Version:
REDBot a Chat bot for a full featured chat bot for Telegram, Facebook Messenger and Slack. Almost no coding skills required
404 lines (389 loc) • 12.7 kB
JavaScript
import React, { useState, useEffect, Fragment, useRef } from 'react';
import gql from 'graphql-tag';
import { useQuery, useApolloClient } from 'react-apollo';
import _ from 'lodash';
import {
Modal,
Button,
Form,
FormGroup,
ControlLabel,
FormControl,
FlexboxGrid,
SelectPicker,
HelpBlock,
Nav
} from 'rsuite';
import { Views } from 'code-plug';
import FieldsEditor from '../../../../src/components/fields-editor';
import MarkdownEditor from '../../../../src/components/markdown-editor';
import ShowError from '../../../../src/components/show-error';
import LanguagePicker from '../../../../src/components/language-picker';
import JSONEditor from '../../../../src/components/json-editor';
import LoaderModal from '../../../../src/components/loader-modal';
import useCanCloseModal from '../../../../src/hooks/modal-can-close';
import useCurrentUser from '../../../../src/hooks/current-user';
import { content as contentModel } from '../models';
import '../styles/modal-content.scss';
import { ModalContentType, ContentViewType } from '../prop-types';
import hasField from '../helpers/has-field';
const LABELS = {
saveContent: 'Save content',
deleteContent: 'Delete',
customFields: 'Custom Fields',
payload: 'Content payload',
title: 'Title',
content: 'Content',
createContent: 'Create content',
editContent: 'Edit content',
slug: 'Slug',
category: 'Category',
tabPayload: 'Payload',
tooltipSlug: `is a shortcut for a content or a group of contents
(for example the same article translated in different languages)`
};
const CATEGORIES = gql`
query($namespace: String) {
categories(namespace: $namespace) {
id,
name
}
}
`;
const GET_CONTENT = gql`
query($id: Int!) {
content(id: $id) {
id,
slug,
title,
body,
categoryId,
language,
createdAt,
payload,
namespace,
category {
id,
name
}
fields {
id,
name,
value,
type
}
}
}
`;
const hasPlugin = (plugins, plugin) => plugins == null || plugins.includes(plugin);
const ContentView = ({
content,
onCancel = () => {},
onSubmit = () => {},
onDelete = () => {},
onChange = () => {},
disabled = false,
hasDelete = false,
error,
fields,
labels = {},
disabledLanguages,
customFieldsSchema,
namespace,
categories,
plugins
}) => {
const { can } = useCurrentUser();
const [formValue, setFormValue] = useState(content);
const [formError, setFormError] = useState(null);
const [jsonValue, setJsonValue] = useState({
json: !_.isEmpty(content.payload) ? JSON.stringify(content.payload, null, 2) : ''
});
const [tab, setTab] = useState('content');
const form = useRef(null);
const isNew = content.id == null;
labels = { ...LABELS, ...labels };
return (
<Fragment>
<Modal.Header>
<Modal.Title>{isNew ? labels.createContent : `${labels.editContent} "${content.title}"`}</Modal.Title>
</Modal.Header>
<Modal.Body>
{error != null && <ShowError error={error}/>}
<Nav
appearance="tabs"
active={tab}
onSelect={tab => {
// tab is switched to manual edit of payload, make sure the current payload field is serialized
// in serialized payload in order to show the updated one
if (tab === 'content-payload') {
setJsonValue({
json: !_.isEmpty(formValue.payload) ? JSON.stringify(formValue.payload, null, 2) : ''
});
}
setTab(tab);
}}
activeKey={tab}
>
<Nav.Item eventKey="content">{labels.content}</Nav.Item>
{hasField(fields, 'custom_fields') && <Nav.Item eventKey="custom_fields">{labels.customFields}</Nav.Item>}
<Views region="content-tabs">
{(View, { label, id, permission }) => {
if ((_.isEmpty(permission) || can(permission)) && hasPlugin(plugins, id)) {
return (
<Nav.Item key={id} active={id === tab} eventKey={id} onSelect={() => setTab(id)}>{label}</Nav.Item>
);
}
}}
</Views>
{hasField(fields, 'payload') && <Nav.Item eventKey="content-payload">{labels.payload}</Nav.Item>}
</Nav>
<Form
model={contentModel}
ref={form}
checkTrigger="none"
formValue={formValue}
formError={formError}
onChange={formValue => {
onChange(formValue);
setFormValue(formValue);
setFormError(null);
}}
onCheck={errors => {
setFormError(errors);
setTab(errors.fields != null ? 'custom_fields' : 'content');
}}
fluid autoComplete="off"
>
{tab === 'content' && (
<Fragment>
<FormGroup>
<ControlLabel>{labels.title}</ControlLabel>
<FormControl name="title"/>
</FormGroup>
{hasField(fields, 'slug', 'categoryId', 'language') && (
<FlexboxGrid justify="space-between" style={{ marginBottom: '20px' }}>
{hasField(fields, 'slug') && (
<FlexboxGrid.Item colspan={7}>
<FormGroup>
<ControlLabel>
{labels.slug}
<HelpBlock tooltip>
<em>{labels.slug}</em> {labels.tooltipSlug}
</HelpBlock>
</ControlLabel>
<FormControl autoComplete="off" readOnly={disabled} name="slug" />
</FormGroup>
</FlexboxGrid.Item>
)}
{hasField(fields, 'categoryId') && (
<FlexboxGrid.Item colspan={7}>
<FormGroup>
<ControlLabel>{labels.category}</ControlLabel>
<FormControl
autoComplete="off"
readOnly={disabled}
name="categoryId"
block
cleanable={false}
data={categories.map(category => ({ value: category.id, label: category.name }))}
accepter={SelectPicker}
/>
</FormGroup>
</FlexboxGrid.Item>
)}
{hasField(fields, 'language') && (
<FlexboxGrid.Item colspan={7}>
<FormGroup>
<ControlLabel>Language</ControlLabel>
<FormControl
readOnly={disabled}
name="language"
disabledItemValues={disabledLanguages}
cleanable={false}
block
accepter={LanguagePicker}
/>
</FormGroup>
</FlexboxGrid.Item>
)}
</FlexboxGrid>
)}
{hasField(fields, 'body') && (
<FormGroup>
<FormControl readOnly={disabled} name="body" accepter={MarkdownEditor}/>
</FormGroup>
)}
<Views region="content-body">
{(View, { id, namespace: pluginNamespace }) => {
if (namespace === pluginNamespace) {
return (
<View
key={id}
formValue={formValue.payload}
onChange={payload => {
onChange({ ...formValue, payload });
setFormValue({ ...formValue, payload });
}}
/>
);
}
}}
</Views>
</Fragment>
)}
{tab === 'custom_fields' && (
<FormGroup>
<FormControl
readOnly={disabled}
name="fields"
accepter={FieldsEditor}
labels={{ addField: labels.addField, availableFields: labels.availableFields }}
schema={customFieldsSchema}
/>
</FormGroup>
)}
<Views region="content-tabs">
{(View, { id }) => {
if (id === tab) {
return (
<View
key={id}
formValue={formValue.payload}
onChange={payload => {
onChange({ ...formValue, payload });
setFormValue({ ...formValue, payload });
}}
/>
);
}
return <div key={id}/>;
}}
</Views>
{tab === 'content-payload' && (
<Form
formValue={jsonValue}
formError={formError}
fluid
autoComplete="off"
>
<FormGroup>
<FormControl
readOnly={disabled}
name="json"
style={{ marginBottom: '20px' }}
accepter={JSONEditor}
onChange={json => {
if (!_.isEmpty(json)) {
let payload;
setJsonValue({ json });
try {
payload = JSON.parse(json);
} catch(e) {
// error do nothing
return;
}
onChange({ ...formValue, payload });
setFormValue({ ...formValue, payload });
}
}}
/>
</FormGroup>
</Form>
)}
</Form>
</Modal.Body>
<Modal.Footer>
{hasDelete && (
<Button
className="btn-delete"
appearance="default"
color="orange"
disabled={disabled}
onClick={() => {
if (confirm(`Delete "${formValue.title}" ?`)) {
onDelete(formValue)
}
}}
>
{labels.deleteContent}
</Button>
)}
<Button onClick={onCancel} appearance="ghost">
Cancel
</Button>
<Button
appearance="primary"
disabled={disabled}
onClick={() => {
if (!form.current.check()) {
return;
}
onSubmit(formValue);
}}
>
{labels.saveContent}
</Button>
</Modal.Footer>
</Fragment>
);
};
ContentView.propTypes = ContentViewType;
const ModalContent = props => {
const [state, setState] = useState({
loading: props.contentId != null,
content: null,
error: null
});
const client = useApolloClient();
const { setIsChanged, handleCancel } = useCanCloseModal({ onCancel: props.onCancel });
const { namespace } = props;
const { loading: loadingCategories, data } = useQuery(CATEGORIES, {
fetchPolicy: 'network-only',
variables: { namespace }
});
useEffect(() => {
if (props.contentId != null) {
client.query({ query: GET_CONTENT, fetchPolicy: 'network-only', variables: { id: props.contentId } })
.then(response => setState({ loading: false, content: response.data.content }))
.catch(error => setState({ loading: false, error }));
}
}, []);
const loading = loadingCategories || state.loading;
let inner;
if (state.error != null) {
inner = (
<div style={{ padding: '80px 10px' }}>
<ShowError error={state.error}/>
<div style={{ marginTop: '20px', textAlign: 'center' }}>
<Button onClick={props.onCancel} appearance="ghost">
Close
</Button>
</div>
</div>
);
} else if (!loading) {
inner = (
<ContentView
{...props}
content={props.contentId != null ? state.content : props.content}
onChange={() => setIsChanged(true)}
onCancel={handleCancel}
categories={data.categories}
/>
);
} else {
inner = (
<div style={{ padding: '80px 10px' }}>
<LoaderModal/>
</div>
);
}
return (
<Modal backdrop show onHide={handleCancel} className="modal-content" overflow={false} size="md">
{inner}
</Modal>
);
};
ModalContent.propTypes = ModalContentType;
export default ModalContent;