Apply PR review

This commit is contained in:
Virginie Ky 2020-01-15 12:17:44 +01:00
parent e250ac81d5
commit 7f60f6eb57
21 changed files with 468 additions and 500 deletions

View File

@ -15,7 +15,6 @@ const Wrapper = styled.div`
line-height: normal;
}
p {
line-height: normal;
&:first-of-type {
font-size: 18px;
font-weight: 600;

View File

@ -5,13 +5,14 @@
*/
import styled from 'styled-components';
import { sizes } from '@buffetjs/styles';
const Wrapper = styled.div`
padding-top: 3px;
padding-bottom: 8px;
table {
width: 100%;
border-radius: 3px;
border-radius: ${sizes.borderRadius};
overflow: hidden;
}
tr {
@ -45,8 +46,8 @@ const Wrapper = styled.div`
}
}
tbody {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
border-bottom-left-radius: ${sizes.borderRadius};
border-bottom-right-radius: ${sizes.borderRadius};
box-shadow: inset 0px 0px 0px 1px #f6f6f6;
td {
height: 54px;

View File

@ -5,13 +5,14 @@
*/
import styled from 'styled-components';
import { sizes } from '@buffetjs/styles';
const Wrapper = styled.div`
margin-top: 12px;
padding: 23px 24px 26px 24px;
background-color: #fafafb;
border-radius: 3px;
ul {
border-radius: ${sizes.borderRadius};
> ul {
margin-bottom: 0;
padding: 0;
list-style-type: none;
@ -29,67 +30,68 @@ const Wrapper = styled.div`
margin-right: 10px;
}
}
}
li {
position: relative;
padding-right: 30px;
&:not(:first-of-type) {
margin-bottom: 20px;
}
&:last-of-type {
margin-bottom: 6px;
}
> section {
display: inline-block;
width: 50%;
vertical-align: top;
&:nth-child(odd) {
padding-right: 15px;
li {
position: relative;
padding-right: 30px;
&:not(:first-of-type) {
margin-bottom: 20px;
}
&:nth-child(even) {
padding-left: 15px;
&:last-of-type {
margin-bottom: 6px;
}
> p {
font-size: 13px;
color: #333740;
font-weight: 500;
}
> div:first-of-type {
height: 34px;
> div:first-of-type {
height: 34px;
min-height: 34px;
border: 1px solid #e3e9f3;
border-radius: 2px;
> section {
display: inline-block;
width: 50%;
vertical-align: top;
&:nth-child(odd) {
padding-right: 15px;
}
&:nth-child(even) {
padding-left: 15px;
}
> p {
font-size: 13px;
color: #333740;
align-items: normal;
font-weight: 500;
}
> div:first-of-type {
height: 34px;
> div:first-of-type {
height: 32px;
padding: 0 1rem;
height: 34px;
min-height: 34px;
border: 1px solid #e3e9f3;
border-radius: 2px;
font-size: 13px;
color: #333740;
align-items: normal;
> div:first-of-type {
height: 32px;
padding: 0 1rem;
}
> div:last-of-type {
display: none;
}
&:hover {
cursor: text;
}
}
> div:last-of-type {
display: none;
}
&:hover {
cursor: text;
> span + div:first-of-type {
border-color: #78caff;
box-shadow: none;
}
}
> span + div:first-of-type {
border-color: #78caff;
box-shadow: none;
}
}
& + div {
position: absolute;
top: 7px;
right: 0;
button {
margin: 0;
& + div {
position: absolute;
top: 7px;
right: 0;
button {
margin: 0;
}
}
}
}
}
.bordered {
input {
border: 1px solid #f64d0a;

View File

@ -7,6 +7,7 @@ import { CircleButton } from 'strapi-helper-plugin';
import { InputText } from '@buffetjs/core';
import { Plus } from '@buffetjs/icons';
import borderColor from './utils/borderColor';
import keys from './keys';
import Wrapper from './Wrapper';
@ -19,8 +20,8 @@ const HeadersInput = ({
onRemove,
value,
}) => {
const optionFormat = value => ({ value: value, label: value });
const options = keys.map(key => optionFormat(key));
const formatOption = value => ({ value: value, label: value });
const options = keys.map(key => formatOption(key));
const handleBlur = () => onBlur({ target: { name, value } });
@ -33,28 +34,14 @@ const HeadersInput = ({
}
};
const handleClick = () => onClick(name);
const handleRemoveItem = index => {
const event = index === 0 && value.length === 1 ? 'clear' : 'remove';
onRemove({ event, index });
};
const customStyles = hasError => {
const selectBorder = isFocused => {
if (isFocused) {
return '1px solid #78caff !important';
}
if (hasError) {
return '1px solid #F64D0A !important';
}
return '1px solid #E3E9F3 !important';
};
return {
control: (base, state) => ({
...base,
border: selectBorder(state.isFocused),
border: `1px solid ${borderColor({
isFocused: state.isFocused,
hasError,
})} !important`,
borderRadius: '2px !important',
}),
menu: base => {
@ -121,7 +108,7 @@ const HeadersInput = ({
name={`${name}.${index}.key`}
options={options}
styles={customStyles(entryErrors && entryErrors.key)}
value={optionFormat(key)}
value={formatOption(key)}
/>
</section>
<section>
@ -137,14 +124,14 @@ const HeadersInput = ({
<CircleButton
type="button"
isRemoveButton
onClick={() => handleRemoveItem(index)}
onClick={() => onRemove(index)}
/>
</div>
</li>
);
})}
</ul>
<button onClick={handleClick} type="button">
<button onClick={() => onClick(name)} type="button">
<Plus fill="#007eff" width="10px" />
<FormattedMessage id="Settings.webhooks.create.header" />
</button>

View File

@ -0,0 +1,11 @@
const borderColor = ({ isFocused = false, hasError = false }) => {
if (isFocused) {
return '#78caff';
}
if (hasError) {
return '#F64D0A';
}
return '#E3E9F3';
};
export default borderColor;

View File

@ -7,7 +7,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { ErrorMessage, Label } from '@buffetjs/styles';
import { Error, InputText } from '@buffetjs/core';
import { Error } from '@buffetjs/core';
import HeadersInput from '../HeadersInput';
import EventInput from '../EventInput';
@ -71,29 +71,15 @@ function Inputs({
return (
<>
{type === 'events' ? (
<EventInput
name={name}
onChange={e => {
handleChange(e);
onBlur(e);
}}
value={value}
/>
) : (
<InputText
className={hasError ? 'hasError' : ''}
name={name}
onBlur={onBlur}
onChange={handleChange}
value={value}
/>
)}
{hasError && (
<ErrorMessage>
<FormattedMessage id={error} />
</ErrorMessage>
)}
<EventInput
name={name}
onChange={e => {
handleChange(e);
onBlur(e);
}}
value={value}
/>
{hasError && <ErrorMessage>{error}</ErrorMessage>}
</>
);
}}

View File

@ -28,7 +28,7 @@ function ListRow({
const links = [
{
icon: 'pencil',
onClick: () => handleEditClick(),
onClick: () => onEditClick(id),
},
{
icon: 'trash',
@ -41,26 +41,19 @@ function ListRow({
const isChecked = itemsToDelete.includes(id);
const handleEditClick = () => onEditClick(id);
const handleEnabledChange = ({ target: { value } }) =>
onEnabledChange(value, id);
const handleCheckChange = ({ target: { value } }) => onCheckChange(value, id);
const handleDeleteConfirm = () => {
onDeleteCLick(id);
const handleDeleteConfirm = async () => {
await onDeleteCLick(id);
setShowModal(false);
};
return (
<StyledListRow onClick={handleEditClick}>
<StyledListRow onClick={() => onEditClick(id)}>
<td>
<Checkbox
name={name}
value={isChecked}
onClick={e => e.stopPropagation()}
onChange={handleCheckChange}
onChange={({ target: { value } }) => onCheckChange(value, id)}
/>
</td>
<td>
@ -74,7 +67,7 @@ function ListRow({
<Switch
name={name}
value={isEnabled}
onChange={handleEnabledChange}
onChange={({ target: { value } }) => onEnabledChange(value, id)}
></Switch>
</div>
</td>

View File

@ -15,13 +15,17 @@ const TriggerContainer = ({ isPending, onCancel, response }) => {
<tbody>
<tr>
<td>
<p>Test-trigger</p>
<p>
{formatMessage({
id: `Settings.webhooks.trigger.test`,
})}
</p>
</td>
{isPending && (
<>
<td>
<p>
<Pending fill="#6DBB1A" width="15px" height="15px" />
<Pending fill="#ffb500" width="15px" height="15px" />
<span>
{formatMessage({
id: `Settings.webhooks.trigger.pending`,
@ -65,7 +69,12 @@ const TriggerContainer = ({ isPending, onCancel, response }) => {
<td>
<p className="fail-label">
<Fail fill="#f64d0a" width="15px" height="15px" />
<span>Error {statusCode}</span>
<span>
{formatMessage({
id: `Settings.error`,
})}{' '}
{statusCode}
</span>
</p>
</td>
<td>

View File

@ -113,10 +113,6 @@ export class Admin extends React.Component {
}, []);
};
renderMarketPlace = props => <Marketplace {...props} {...this.props} />;
renderSettings = props => <SettingsPage {...props} {...this.props} />;
renderPluginDispatcher = props => {
// NOTE: Send the needed props instead of everything...
@ -183,10 +179,12 @@ export class Admin extends React.Component {
/>
<Route
path="/marketplace"
render={this.renderMarketPlace}
exact
render={props => this.renderRoute(props, Marketplace)}
/>
<Route
path="/settings"
render={props => this.renderRoute(props, SettingsPage)}
/>
<Route path="/settings" render={this.renderSettings} />
<Route key="7" path="" component={NotFoundPage} />
<Route key="8" path="404" component={NotFoundPage} />
</Switch>

View File

@ -32,9 +32,7 @@ function SettingsPage() {
<div className="col-md-3">
<LeftMenu>
{menuItems.map(item => {
return (
<LeftMenuList {...item} key={JSON.stringify(item.title)} />
);
return <LeftMenuList {...item} key={item.title.id} />;
})}
</LeftMenu>
</div>

View File

@ -32,6 +32,8 @@ const Wrapper = styled.div`
}
span svg {
margin-top: -2px;
margin-right: -10px;
margin-bottom: -5px;
}
}
}

View File

@ -5,9 +5,9 @@
*/
import React, { useEffect, useReducer, useCallback, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { get, isEmpty, isEqual, set, setWith } from 'lodash';
import { Header } from '@buffetjs/custom';
import { useHistory, useParams } from 'react-router-dom';
import { get, isEmpty, isEqual, set } from 'lodash';
import { Header, Inputs as InputsIndex } from '@buffetjs/custom';
import { Play } from '@buffetjs/icons';
import {
request,
@ -21,7 +21,8 @@ import TriggerContainer from '../../../components/TriggerContainer';
import reducer, { initialState } from './reducer';
import form from './utils/form';
import createYupSchema from './utils/schema';
import schema from './utils/schema';
import { cleanHeaders, cleanData, cleanErrors } from './utils/formatData';
import Wrapper from './Wrapper';
@ -29,31 +30,105 @@ function EditView() {
const { formatMessage } = useGlobalContext();
const [submittedOnce, setSubmittedOnce] = useState(false);
const [reducerState, dispatch] = useReducer(reducer, initialState);
const location = useLocation();
const { push } = useHistory();
const {
formErrors,
modifiedWebhook,
initialWebhook,
modifiedData,
initialData,
isTriggering,
triggerResponse,
} = reducerState.toJS();
const { name } = modifiedWebhook;
const id = location.pathname.split('/')[3];
const { name } = modifiedData;
const { id } = useParams();
const isCreating = id === 'create';
const abortController = new AbortController();
const { signal } = abortController;
useEffect(() => {
if (!isCreating) {
fetchData();
return () => {
abortController.abort();
};
}
}, [fetchData, isCreating]);
}, [abortController, fetchData, isCreating]);
/* Header props */
const areActionDisabled =
isEqual(initialData, modifiedData) || Object.keys(formErrors).length > 0;
const headerTitle = isCreating
? formatMessage({
id: `Settings.webhooks.create`,
})
: name;
const isTriggerActionDisabled =
isCreating || (!isCreating && !areActionDisabled) || isTriggering;
const actions = [
{
color: 'primary',
disabled: isTriggerActionDisabled,
label: formatMessage({
id: `Settings.webhooks.trigger`,
}),
onClick: handleTrigger,
style: {
padding: '0 15px',
},
title: isTriggerActionDisabled
? formatMessage({
id: `Settings.webhooks.trigger.save`,
})
: null,
type: 'button',
icon: (
<Play
width="14px"
height="14px"
fill={isTriggerActionDisabled ? '#b4b6ba' : '#ffffff'}
/>
),
},
{
color: 'cancel',
disabled: areActionDisabled,
label: formatMessage({
id: `app.components.Button.reset`,
}),
onClick: handleReset,
style: {
padding: '0 20px',
},
type: 'button',
},
{
color: 'success',
disabled: areActionDisabled,
label: formatMessage({
id: `app.components.Button.save`,
}),
style: {
minWidth: 140,
},
type: 'submit',
},
];
const headerProps = {
title: {
label: headerTitle,
},
actions: actions,
};
/* Data methods */
const fetchData = useCallback(async () => {
try {
@ -72,115 +147,57 @@ function EditView() {
}
}, [id]);
const headerTitle = isCreating
? formatMessage({
id: `Settings.webhooks.create`,
})
: name;
const actionsAreDisabled =
isEqual(initialWebhook, modifiedWebhook) ||
Object.keys(formErrors).length > 0;
const triggerActionIsDisabled =
isCreating || (!isCreating && !actionsAreDisabled);
const handleTrigger = async () => {
dispatch({
type: 'ON_TRIGGER',
});
try {
const { data } = await request(`/admin/webhooks/${id}/trigger`, {
method: 'POST',
signal,
});
dispatch({
type: 'TRIGGER_SUCCEEDED',
response: data,
});
} catch (err) {
if (err.code !== 20) {
strapi.notification.error('notification.error');
}
const submitForm = () => {
if (!isCreating) {
updateWebhook();
} else {
createWebhooks();
}
};
const onCancelTrigger = () => {
abortController.abort();
const createWebhooks = async () => {
try {
await request(`/admin/webhooks`, {
method: 'POST',
body: cleanData(modifiedData),
});
dispatch({
type: 'ON_TRIGGER_CANCELED',
});
strapi.notification.success(`notification.success`);
goBack();
} catch (err) {
strapi.notification.error('notification.error');
}
};
const updateWebhook = async () => {
try {
const body = cleanData(modifiedData);
delete body.id;
await request(`/admin/webhooks/${id}`, {
method: 'PUT',
body,
});
fetchData();
strapi.notification.error('notification.form.success.fields');
} catch (err) {
strapi.notification.error('notification.error');
}
};
/* Form user interactions */
const handleReset = () =>
dispatch({
type: 'RESET',
type: 'RESET_FORM',
});
const actions = [
{
color: 'primary',
disabled: triggerActionIsDisabled,
label: formatMessage({
id: `Settings.webhooks.trigger`,
}),
onClick: () => {
handleTrigger();
},
style: {
padding: '0 15px',
},
title: triggerActionIsDisabled
? formatMessage({
id: `Settings.webhooks.trigger.save`,
})
: null,
type: 'button',
icon: (
<Play
width="6px"
height="7px"
fill={triggerActionIsDisabled ? '#b4b6ba' : '#ffffff'}
/>
),
},
{
color: 'cancel',
disabled: actionsAreDisabled,
label: formatMessage({
id: `app.components.Button.reset`,
}),
onClick: () => handleReset(),
style: {
padding: '0 20px',
},
type: 'button',
},
{
color: 'success',
disabled: actionsAreDisabled,
label: formatMessage({
id: `app.components.Button.save`,
}),
style: {
minWidth: 140,
},
type: 'submit',
},
];
const headerProps = {
title: {
label: headerTitle,
},
actions: actions,
};
const handleBlur = () => {
if (submittedOnce) checkFormErrors();
if (submittedOnce) {
checkFormErrors();
}
};
const handleChange = ({ target: { name, value } }) => {
@ -198,11 +215,10 @@ function EditView() {
});
};
const handleRemove = ({ event, index }) => {
const handleRemove = index => {
dispatch({
type: 'ON_HEADER_REMOVE',
index,
event,
});
resetError('headers');
};
@ -213,20 +229,14 @@ function EditView() {
checkFormErrors(true);
};
const submitForm = () => {
if (!isCreating) {
updateWebhook();
} else {
createWebhooks();
}
};
/* Validations */
const checkFormErrors = async (submit = false) => {
const webhookToCheck = modifiedWebhook;
set(webhookToCheck, 'headers', cleanHeaders());
const webhookToCheck = modifiedData;
set(webhookToCheck, 'headers', cleanHeaders(modifiedData.headers));
try {
await createYupSchema(form).validate(webhookToCheck, {
await schema.validate(webhookToCheck, {
abortEarly: false,
});
@ -238,18 +248,6 @@ function EditView() {
}
};
const cleanHeaders = () => {
const { headers } = modifiedWebhook;
if (Object.keys(headers).length === 1) {
const { key, value } = headers[0];
if (key.length === 0 && value.length === 0) return [];
}
return headers;
};
const goBack = () => push('/settings/webhooks');
const resetError = name => {
const errors = formErrors;
@ -260,68 +258,66 @@ function EditView() {
};
const setErrors = errors => {
const newErrors = Object.keys(errors).reduce((acc, curr) => {
const { id } = errors[curr];
setWith(acc, curr, id ? id : errors[curr], Object);
return acc;
}, {});
dispatch({
type: 'SET_ERRORS',
errors: newErrors,
errors: cleanErrors(errors),
});
};
const createWebhooks = async () => {
const errorMessage = error => {
if (!error) {
return null;
}
if (typeof error === 'string') {
return formatMessage({
id: error,
});
}
return error;
};
/* Trigger events */
const handleTrigger = async () => {
dispatch({
type: 'IS_TRIGGERING',
});
try {
await request(`/admin/webhooks`, {
const { data } = await request(`/admin/webhooks/${id}/trigger`, {
method: 'POST',
body: formatWebhook(),
signal,
});
strapi.notification.success(`notification.success`);
goBack();
dispatch({
type: 'TRIGGER_SUCCEEDED',
response: data,
});
} catch (err) {
strapi.notification.error('notification.error');
if (err.code !== 20) {
strapi.notification.error('notification.error');
}
dispatch({
type: 'IS_TRIGGERING',
});
}
};
const updateWebhook = async () => {
try {
const body = formatWebhook();
delete body.id;
const onCancelTrigger = () => {
abortController.abort();
await request(`/admin/webhooks/${id}`, {
method: 'PUT',
body,
});
fetchData();
strapi.notification.error('notification.form.success.fields');
} catch (err) {
strapi.notification.error('notification.error');
}
dispatch({
type: 'ON_TRIGGER_CANCELED',
});
};
// utils
const formatWebhook = () => {
const webhooks = modifiedWebhook;
set(webhooks, 'headers', unformatHeaders(cleanHeaders()));
return webhooks;
};
/* Nav */
const unformatHeaders = headers => {
return headers.reduce((obj, item) => {
const { key, value } = item;
return {
...obj,
[key]: value,
};
}, {});
};
const goBack = () => push('/settings/webhooks');
const renderHeadersInput = () => props => (
<Inputs {...props} onClick={handleClick} onRemove={handleRemove} />
);
return (
<Wrapper>
@ -343,16 +339,18 @@ function EditView() {
{Object.keys(form).map(key => {
return (
<div key={key} className={form[key].styleName}>
<Inputs
<InputsIndex
{...form[key]}
error={get(formErrors, key, null)}
customInputs={{
headers: renderHeadersInput(),
events: Inputs,
}}
error={errorMessage(get(formErrors, key, null))}
name={key}
onBlur={handleBlur}
onChange={handleChange}
onClick={handleClick}
onRemove={handleRemove}
validations={form[key].validations}
value={modifiedWebhook[key] || form[key].value}
value={modifiedData[key] || form[key].value}
/>
</div>
);

View File

@ -1,79 +1,74 @@
import { fromJS } from 'immutable';
import { cloneDeep, set, get } from 'lodash';
import { get } from 'lodash';
const header = { key: '', value: '' };
const initialWebhook = {
events: [],
headers: [header],
name: null,
url: null,
headers: [{ key: '', value: '' }],
events: [],
};
const initialState = fromJS({
formErrors: {},
initialWebhook: initialWebhook,
modifiedWebhook: initialWebhook,
triggerResponse: {},
initialData: initialWebhook,
isTriggering: false,
modifiedData: initialWebhook,
triggerResponse: {},
});
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_NEW_HEADER':
return state.updateIn(['modifiedData', ...action.keys], arr =>
arr.push(fromJS(header))
);
case 'GET_DATA_SUCCEEDED': {
const data = cloneDeep(action.data);
const headers = get(data, 'headers');
const headers = get(action, ['data', 'headers'], {});
let formattedHeaders = [header];
if (Object.keys(headers).length > 0) {
const newHeaders = fromJS(
Object.keys(headers).map(key => {
return { key: key, value: headers[key] };
})
);
set(data, ['headers'], newHeaders);
} else {
set(data, ['headers'], get(initialWebhook, 'headers'));
formattedHeaders = Object.keys(headers).map(key => {
return { key: key, value: headers[key] };
});
}
const data = fromJS(action.data).update('headers', () =>
fromJS(formattedHeaders)
);
return state
.update('initialWebhook', () => fromJS(data))
.update('modifiedWebhook', () => fromJS(data));
.update('initialData', () => data)
.update('modifiedData', () => data);
}
case 'IS_TRIGGERING':
return state.update('isTriggering', isTriggering => !isTriggering);
case 'ON_CHANGE':
return state.updateIn(
['modifiedData', ...action.keys],
() => action.value
);
case 'ON_HEADER_REMOVE': {
return state.updateIn(['modifiedData', 'headers'], headers => {
if (headers.size === 1) {
return fromJS([header]);
}
return headers.remove(action.index);
});
}
case 'ON_TRIGGER_CANCELED':
return state
.update('isTriggering', () => false)
.set('triggerResponse', fromJS({}));
case 'RESET_FORM':
return state.update('modifiedData', () => state.get('initialData'));
case 'SET_ERRORS':
return state.update('formErrors', () => fromJS(action.errors));
case 'TRIGGER_SUCCEEDED':
return state
.update('triggerResponse', () => fromJS(action.response))
.update('isTriggering', () => false);
case 'ON_TRIGGER':
return state.update('isTriggering', () => true);
case 'ON_TRIGGER_CANCELED':
return state
.update('isTriggering', () => false)
.update('triggerResponse', () => {});
case 'ON_CHANGE':
return state.updateIn(
['modifiedWebhook', ...action.keys],
() => action.value
);
case 'ADD_NEW_HEADER':
return state.updateIn(['modifiedWebhook', ...action.keys], arr =>
arr.push(fromJS({ key: '', value: '' }))
);
case 'ON_HEADER_REMOVE': {
if (action.event === 'remove') {
return state.updateIn(['modifiedWebhook', 'headers'], headers =>
headers.splice(action.index, 1)
);
} else {
return state.updateIn(
['modifiedWebhook', 'headers', action.index],
() => fromJS({ key: '', value: '' })
);
}
}
case 'RESET':
return state.update('modifiedWebhook', () =>
fromJS(state.get('initialWebhook'))
);
case 'SET_ERRORS':
return state.update('formErrors', () => fromJS(action.errors));
default:
return state;
}

View File

@ -0,0 +1,4 @@
const NAME_REGEX = new RegExp('(^$)|(^[A-Za-z][_0-9A-Za-z ]*$)');
const URL_REGEX = new RegExp('(^$)|((https?://.*)(d*)/?(.*))');
export { NAME_REGEX, URL_REGEX };

View File

@ -4,36 +4,24 @@ const form = {
label: 'Name',
type: 'text',
value: '',
validations: {
required: true,
regex: new RegExp('(^$)|(^[A-Za-z][_0-9A-Za-z ]*$)'),
},
},
url: {
styleName: 'col-12',
label: 'URL',
type: 'text',
value: '',
validations: {
required: true,
regex: new RegExp('(^$)|((https?://.*)(d*)/?(.*))'),
},
},
headers: {
styleName: 'col-12',
label: 'Headers',
type: 'headers',
value: [{ key: '', value: '' }],
validations: {},
},
events: {
styleName: 'col-12',
label: 'Hooks',
type: 'events',
value: [],
validations: {
required: true,
},
},
};

View File

@ -0,0 +1,40 @@
import { set, setWith } from 'lodash';
const cleanData = data => {
const webhooks = data;
const headers = cleanHeaders(data.headers);
set(webhooks, 'headers', unformatHeaders(headers));
return webhooks;
};
const unformatHeaders = headers => {
return headers.reduce((obj, item) => {
const { key, value } = item;
return {
...obj,
[key]: value,
};
}, {});
};
const cleanHeaders = headers => {
if (Object.keys(headers).length === 1) {
const { key, value } = headers[0];
if (key.length === 0 && value.length === 0) {
return [];
}
}
return headers;
};
const cleanErrors = errors => {
return Object.keys(errors).reduce((acc, curr) => {
const { id } = errors[curr];
setWith(acc, curr, id ? id : errors[curr], Object);
return acc;
}, {});
};
export { cleanHeaders, cleanData, cleanErrors };

View File

@ -1,54 +1,31 @@
import * as yup from 'yup';
import { translatedErrors } from 'strapi-helper-plugin';
import { NAME_REGEX, URL_REGEX } from './fieldsRegex';
const createYupSchema = form =>
yup.object().shape(
Object.keys(form).reduce((acc, current) => {
const { type, validations } = form[current];
acc[current] = createYupSchemaEntry(type, validations);
const schema = yup.object().shape({
name: yup
.string(translatedErrors.string)
.nullable()
.required(translatedErrors.required)
.matches(NAME_REGEX, translatedErrors.regex),
url: yup
.string(translatedErrors.string)
.nullable()
.required(translatedErrors.required)
.matches(URL_REGEX, translatedErrors.regex),
headers: yup
.array()
.of(
yup.object().shape({
key: yup.string().required(),
value: yup.string().required(),
})
)
.nullable(),
events: yup
.array()
.nullable()
.required(translatedErrors.required),
});
return acc;
}, {})
);
const createYupSchemaEntry = (type, validations) => {
let schema = yup.mixed();
if (['text'].includes(type)) {
schema = yup.string(translatedErrors.string).nullable();
}
if (['headers'].includes(type)) {
schema = yup
.array()
.of(
yup.object().shape({
key: yup.string().required(),
value: yup.string().required(),
})
)
.nullable();
}
if (['events'].includes(type)) {
schema = yup.array();
}
Object.keys(validations).forEach(validation => {
const validationValue = validations[validation];
switch (validation) {
case 'required':
schema = schema.required(translatedErrors.required);
break;
case 'regex':
schema = schema.matches(validationValue, translatedErrors.regex);
break;
default:
}
});
return schema;
};
export default createYupSchema;
export default schema;

View File

@ -26,25 +26,19 @@ import reducer, { initialState } from './reducer';
function ListView() {
const { formatMessage } = useGlobalContext();
const [webhooksToDelete, setWebhooksToDelete] = useState([]);
const [showModal, setShowModal] = useState(false);
const [reducerState, dispatch] = useReducer(reducer, initialState);
const { push } = useHistory();
const location = useLocation();
const { pathname } = useLocation();
const { shouldRefetchData, webhooks } = reducerState.toJS();
const { webhooks, webhooksToDelete } = reducerState.toJS();
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
if (shouldRefetchData) {
fetchData();
}
}, [shouldRefetchData]);
const webhookIndex = id => webhooks.findIndex(webhook => webhook.id === id);
const getWebhookIndex = id =>
webhooks.findIndex(webhook => webhook.id === id);
const fetchData = async () => {
try {
@ -70,7 +64,7 @@ function ListView() {
const newButtonProps = {
label: addBtnLabel,
onClick: () => handleCreateClick(),
onClick: () => handleGoTo('create'),
color: 'primary',
type: 'button',
icon: <Plus fill="#007eff" width="11px" height="11px" />,
@ -119,32 +113,27 @@ function ListView() {
items: webhooks,
};
const handleCheckChange = (value, id) => {
if (value && !webhooksToDelete.includes(id)) {
setWebhooksToDelete([...webhooksToDelete, id]);
}
const handleChange = (value, id) => {
const updatedWebhooksToDelete = value
? [...webhooksToDelete, id]
: webhooksToDelete.filter(webhookId => webhookId !== id);
if (!value && webhooksToDelete.includes(id)) {
setWebhooksToDelete([
...webhooksToDelete.filter(webhookId => webhookId !== id),
]);
}
dispatch({
type: 'SET_WEBHOOKS_TO_DELETE',
webhooks: updatedWebhooksToDelete,
});
};
const handleCreateClick = () => {
push(`${location.pathname}/create`);
const handleGoTo = to => {
push(`${pathname}/${to}`);
};
const handleEditClick = id => {
push(`${location.pathname}/${id}`);
};
const handleDeleteAllConfirm = () => {
handleDeleteAllClick();
const handleDeleteAllConfirm = async () => {
await onDeleteAllCLick();
setShowModal(false);
};
const handleDeleteAllClick = async () => {
const onDeleteAllCLick = async () => {
const body = {
ids: webhooksToDelete,
};
@ -158,8 +147,6 @@ function ListView() {
dispatch({
type: 'WEBHOOKS_DELETED',
});
setWebhooksToDelete([]);
} catch (err) {
if (err.code !== 20) {
strapi.notification.error('notification.error');
@ -167,11 +154,12 @@ function ListView() {
}
};
const handleDeleteClick = id => {
deleteWebhook(id);
const handleDeleteConfirm = async id => {
await onDeleteCLick(id);
setShowModal(false);
};
const deleteWebhook = async id => {
const onDeleteCLick = async id => {
try {
await request(`/admin/webhooks/${id}`, {
method: 'DELETE',
@ -179,7 +167,7 @@ function ListView() {
dispatch({
type: 'WEBHOOK_DELETED',
index: webhookIndex(id),
index: getWebhookIndex(id),
});
} catch (err) {
if (err.code !== 20) {
@ -189,7 +177,10 @@ function ListView() {
};
const handleEnabledChange = async (value, id) => {
const initialWebhookProps = webhooks[webhookIndex(id)];
const webhookIndex = getWebhookIndex(id);
const initialWebhookProps = webhooks[webhookIndex];
const keys = [webhookIndex, 'isEnabled'];
const body = {
...initialWebhookProps,
@ -199,17 +190,23 @@ function ListView() {
delete body.id;
try {
dispatch({
type: 'SET_WEBHOOK_ENABLED',
keys,
value: value,
});
await request(`/admin/webhooks/${id}`, {
method: 'PUT',
body,
});
} catch (err) {
dispatch({
type: 'SET_WEBHOOK_ENABLED',
keys: [webhookIndex(id), 'isEnabled'],
value: value,
keys,
value: !value,
});
} catch (err) {
if (err.code !== 20) {
strapi.notification.error('notification.error');
}
@ -227,9 +224,9 @@ function ListView() {
return (
<ListRow
{...props}
onCheckChange={handleCheckChange}
onEditClick={handleEditClick}
onDeleteCLick={handleDeleteClick}
onCheckChange={handleChange}
onEditClick={handleGoTo}
onDeleteCLick={handleDeleteConfirm}
onEnabledChange={handleEnabledChange}
itemsToDelete={webhooksToDelete}
/>

View File

@ -2,23 +2,29 @@ import { fromJS } from 'immutable';
const initialState = fromJS({
webhooks: [],
shouldRefetchData: false,
webhooksToDelete: [],
});
const reducer = (state, action) => {
switch (action.type) {
case 'GET_DATA_SUCCEEDED':
return state
.update('webhooks', () => fromJS(action.data))
.update('shouldRefetchData', () => false);
return state.update('webhooks', () => fromJS(action.data));
case 'SET_WEBHOOK_ENABLED':
return state.updateIn(['webhooks', ...action.keys], () => action.value);
case 'SET_WEBHOOKS_TO_DELETE':
return state.update('webhooksToDelete', () => action.webhooks);
case 'WEBHOOKS_DELETED':
return state
.update('webhooks', webhooks =>
webhooks.filter(webhook => {
return !state.get('webhooksToDelete').includes(webhook.get('id'));
})
)
.update('webhooksToDelete', () => []);
case 'WEBHOOK_DELETED':
return state.update('webhooks', webhooks =>
webhooks.splice(action.index, 1)
);
case 'WEBHOOKS_DELETED':
return state.update('shouldRefetchData', v => !v);
default:
return state;
}

View File

@ -221,6 +221,7 @@
"Auth.link.forgot-password": "Forgot your password?",
"Auth.link.ready": "Ready to sign in?",
"Settings.global": "Global Settings",
"Settings.error": "Error",
"Settings.webhooks.title": "Webhooks",
"Settings.webhooks.singular": "webhook",
"Settings.webhooks.list.description": "Get POST changes notifications.",
@ -242,6 +243,7 @@
"Settings.webhooks.trigger.success": "Success!",
"Settings.webhooks.trigger.success.label": "Trigger succeded",
"Settings.webhooks.trigger.save": "Please save to trigger",
"Settings.webhooks.trigger.test": "Test-trigger",
"Settings.webhooks.events.create": "Create",
"Settings.webhooks.events.edit": "Edit",
"Settings.webhooks.events.delete": "Delete",

View File

@ -1,26 +1 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="23px" height="32px" viewBox="0 0 23 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Inputs/Select-(col-6)" transform="translate(-335.000000, -1.000000)">
<g id="input">
<g id="Number" transform="translate(335.017699, 1.000000)">
<g id="Group">
<path d="M-5.68434189e-14,0 L20,0 L20,0 C21.1045695,-2.02906125e-16 22,0.8954305 22,2 L22,30 L22,30 C22,31.1045695 21.1045695,32 20,32 L-5.68434189e-14,32 L-5.68434189e-14,0 Z" id="Rectangle" fill="#FAFAFB"></path>
<g id="Carets" transform="translate(8.000000, 11.000000)" fill-rule="nonzero" fill="#B3B5B9">
<g id="caret-down" transform="translate(0.000000, 7.000000)">
<path d="M6,0.375 C6,0.4765625 5.96289062,0.564453125 5.88867188,0.638671875 L3.26367188,3.26367188 C3.18945312,3.33789063 3.1015625,3.375 3,3.375 C2.8984375,3.375 2.81054688,3.33789063 2.73632812,3.26367188 L0.111328125,0.638671875 C0.037109375,0.564453125 0,0.4765625 0,0.375 C0,0.2734375 0.037109375,0.185546875 0.111328125,0.111328125 C0.185546875,0.037109375 0.2734375,0 0.375,0 L5.625,0 C5.7265625,0 5.81445312,0.037109375 5.88867188,0.111328125 C5.96289062,0.185546875 6,0.2734375 6,0.375 Z" id="Shape"></path>
</g>
<g id="caret-top" transform="translate(3.000000, 2.375000) rotate(180.000000) translate(-3.000000, -2.375000) translate(0.000000, 0.375000)">
<path d="M6,0.375 C6,0.4765625 5.96289062,0.564453125 5.88867187,0.638671875 L3.26367187,3.26367188 C3.18945312,3.33789063 3.1015625,3.375 3,3.375 C2.8984375,3.375 2.81054687,3.33789063 2.73632812,3.26367188 L0.111328125,0.638671875 C0.037109375,0.564453125 -1.77635684e-15,0.4765625 -1.77635684e-15,0.375 C-1.77635684e-15,0.2734375 0.037109375,0.185546875 0.111328125,0.111328125 C0.185546875,0.037109375 0.2734375,1.77635684e-15 0.375,1.77635684e-15 L5.625,1.77635684e-15 C5.7265625,1.77635684e-15 5.81445312,0.037109375 5.88867187,0.111328125 C5.96289062,0.185546875 6,0.2734375 6,0.375 Z" id="Shape"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
<svg width="23" height="32" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M.018 0h20a2 2 0 012 2v28a2 2 0 01-2 2h-20V0z" fill="#FAFAFB"/><g fill-rule="nonzero" fill="#B3B5B9"><path d="M14.018 18.375a.36.36 0 01-.112.264l-2.625 2.625a.36.36 0 01-.263.111.36.36 0 01-.264-.111l-2.625-2.625a.36.36 0 01-.111-.264.36.36 0 01.111-.264.36.36 0 01.264-.111h5.25a.36.36 0 01.263.111.36.36 0 01.112.264zM8.018 15a.36.36 0 01.111-.264l2.625-2.625a.36.36 0 01.264-.111.36.36 0 01.263.111l2.625 2.625a.36.36 0 01.112.264.36.36 0 01-.112.264.36.36 0 01-.263.111h-5.25a.36.36 0 01-.264-.111.36.36 0 01-.111-.264z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 648 B