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; line-height: normal;
} }
p { p {
line-height: normal;
&:first-of-type { &:first-of-type {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;

View File

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

View File

@ -5,13 +5,14 @@
*/ */
import styled from 'styled-components'; import styled from 'styled-components';
import { sizes } from '@buffetjs/styles';
const Wrapper = styled.div` const Wrapper = styled.div`
margin-top: 12px; margin-top: 12px;
padding: 23px 24px 26px 24px; padding: 23px 24px 26px 24px;
background-color: #fafafb; background-color: #fafafb;
border-radius: 3px; border-radius: ${sizes.borderRadius};
ul { > ul {
margin-bottom: 0; margin-bottom: 0;
padding: 0; padding: 0;
list-style-type: none; list-style-type: none;
@ -29,7 +30,6 @@ const Wrapper = styled.div`
margin-right: 10px; margin-right: 10px;
} }
} }
}
li { li {
position: relative; position: relative;
padding-right: 30px; padding-right: 30px;
@ -90,6 +90,8 @@ const Wrapper = styled.div`
} }
} }
} }
}
.bordered { .bordered {
input { input {
border: 1px solid #f64d0a; border: 1px solid #f64d0a;

View File

@ -7,6 +7,7 @@ import { CircleButton } from 'strapi-helper-plugin';
import { InputText } from '@buffetjs/core'; import { InputText } from '@buffetjs/core';
import { Plus } from '@buffetjs/icons'; import { Plus } from '@buffetjs/icons';
import borderColor from './utils/borderColor';
import keys from './keys'; import keys from './keys';
import Wrapper from './Wrapper'; import Wrapper from './Wrapper';
@ -19,8 +20,8 @@ const HeadersInput = ({
onRemove, onRemove,
value, value,
}) => { }) => {
const optionFormat = value => ({ value: value, label: value }); const formatOption = value => ({ value: value, label: value });
const options = keys.map(key => optionFormat(key)); const options = keys.map(key => formatOption(key));
const handleBlur = () => onBlur({ target: { name, value } }); 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 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 { return {
control: (base, state) => ({ control: (base, state) => ({
...base, ...base,
border: selectBorder(state.isFocused), border: `1px solid ${borderColor({
isFocused: state.isFocused,
hasError,
})} !important`,
borderRadius: '2px !important', borderRadius: '2px !important',
}), }),
menu: base => { menu: base => {
@ -121,7 +108,7 @@ const HeadersInput = ({
name={`${name}.${index}.key`} name={`${name}.${index}.key`}
options={options} options={options}
styles={customStyles(entryErrors && entryErrors.key)} styles={customStyles(entryErrors && entryErrors.key)}
value={optionFormat(key)} value={formatOption(key)}
/> />
</section> </section>
<section> <section>
@ -137,14 +124,14 @@ const HeadersInput = ({
<CircleButton <CircleButton
type="button" type="button"
isRemoveButton isRemoveButton
onClick={() => handleRemoveItem(index)} onClick={() => onRemove(index)}
/> />
</div> </div>
</li> </li>
); );
})} })}
</ul> </ul>
<button onClick={handleClick} type="button"> <button onClick={() => onClick(name)} type="button">
<Plus fill="#007eff" width="10px" /> <Plus fill="#007eff" width="10px" />
<FormattedMessage id="Settings.webhooks.create.header" /> <FormattedMessage id="Settings.webhooks.create.header" />
</button> </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 PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { ErrorMessage, Label } from '@buffetjs/styles'; import { ErrorMessage, Label } from '@buffetjs/styles';
import { Error, InputText } from '@buffetjs/core'; import { Error } from '@buffetjs/core';
import HeadersInput from '../HeadersInput'; import HeadersInput from '../HeadersInput';
import EventInput from '../EventInput'; import EventInput from '../EventInput';
@ -71,7 +71,6 @@ function Inputs({
return ( return (
<> <>
{type === 'events' ? (
<EventInput <EventInput
name={name} name={name}
onChange={e => { onChange={e => {
@ -80,20 +79,7 @@ function Inputs({
}} }}
value={value} value={value}
/> />
) : ( {hasError && <ErrorMessage>{error}</ErrorMessage>}
<InputText
className={hasError ? 'hasError' : ''}
name={name}
onBlur={onBlur}
onChange={handleChange}
value={value}
/>
)}
{hasError && (
<ErrorMessage>
<FormattedMessage id={error} />
</ErrorMessage>
)}
</> </>
); );
}} }}

View File

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

View File

@ -15,13 +15,17 @@ const TriggerContainer = ({ isPending, onCancel, response }) => {
<tbody> <tbody>
<tr> <tr>
<td> <td>
<p>Test-trigger</p> <p>
{formatMessage({
id: `Settings.webhooks.trigger.test`,
})}
</p>
</td> </td>
{isPending && ( {isPending && (
<> <>
<td> <td>
<p> <p>
<Pending fill="#6DBB1A" width="15px" height="15px" /> <Pending fill="#ffb500" width="15px" height="15px" />
<span> <span>
{formatMessage({ {formatMessage({
id: `Settings.webhooks.trigger.pending`, id: `Settings.webhooks.trigger.pending`,
@ -65,7 +69,12 @@ const TriggerContainer = ({ isPending, onCancel, response }) => {
<td> <td>
<p className="fail-label"> <p className="fail-label">
<Fail fill="#f64d0a" width="15px" height="15px" /> <Fail fill="#f64d0a" width="15px" height="15px" />
<span>Error {statusCode}</span> <span>
{formatMessage({
id: `Settings.error`,
})}{' '}
{statusCode}
</span>
</p> </p>
</td> </td>
<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 => { renderPluginDispatcher = props => {
// NOTE: Send the needed props instead of everything... // NOTE: Send the needed props instead of everything...
@ -183,10 +179,12 @@ export class Admin extends React.Component {
/> />
<Route <Route
path="/marketplace" path="/marketplace"
render={this.renderMarketPlace} render={props => this.renderRoute(props, Marketplace)}
exact />
<Route
path="/settings"
render={props => this.renderRoute(props, SettingsPage)}
/> />
<Route path="/settings" render={this.renderSettings} />
<Route key="7" path="" component={NotFoundPage} /> <Route key="7" path="" component={NotFoundPage} />
<Route key="8" path="404" component={NotFoundPage} /> <Route key="8" path="404" component={NotFoundPage} />
</Switch> </Switch>

View File

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

View File

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

View File

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

View File

@ -1,79 +1,74 @@
import { fromJS } from 'immutable'; import { fromJS } from 'immutable';
import { cloneDeep, set, get } from 'lodash'; import { get } from 'lodash';
const header = { key: '', value: '' };
const initialWebhook = { const initialWebhook = {
events: [],
headers: [header],
name: null, name: null,
url: null, url: null,
headers: [{ key: '', value: '' }],
events: [],
}; };
const initialState = fromJS({ const initialState = fromJS({
formErrors: {}, formErrors: {},
initialWebhook: initialWebhook, initialData: initialWebhook,
modifiedWebhook: initialWebhook,
triggerResponse: {},
isTriggering: false, isTriggering: false,
modifiedData: initialWebhook,
triggerResponse: {},
}); });
const reducer = (state, action) => { const reducer = (state, action) => {
switch (action.type) { switch (action.type) {
case 'ADD_NEW_HEADER':
return state.updateIn(['modifiedData', ...action.keys], arr =>
arr.push(fromJS(header))
);
case 'GET_DATA_SUCCEEDED': { case 'GET_DATA_SUCCEEDED': {
const data = cloneDeep(action.data); const headers = get(action, ['data', 'headers'], {});
const headers = get(data, 'headers'); let formattedHeaders = [header];
if (Object.keys(headers).length > 0) { if (Object.keys(headers).length > 0) {
const newHeaders = fromJS( formattedHeaders = Object.keys(headers).map(key => {
Object.keys(headers).map(key => {
return { key: key, value: headers[key] }; return { key: key, value: headers[key] };
}) });
);
set(data, ['headers'], newHeaders);
} else {
set(data, ['headers'], get(initialWebhook, 'headers'));
} }
const data = fromJS(action.data).update('headers', () =>
fromJS(formattedHeaders)
);
return state return state
.update('initialWebhook', () => fromJS(data)) .update('initialData', () => data)
.update('modifiedWebhook', () => fromJS(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': case 'TRIGGER_SUCCEEDED':
return state return state
.update('triggerResponse', () => fromJS(action.response)) .update('triggerResponse', () => fromJS(action.response))
.update('isTriggering', () => false); .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: default:
return state; 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', label: 'Name',
type: 'text', type: 'text',
value: '', value: '',
validations: {
required: true,
regex: new RegExp('(^$)|(^[A-Za-z][_0-9A-Za-z ]*$)'),
},
}, },
url: { url: {
styleName: 'col-12', styleName: 'col-12',
label: 'URL', label: 'URL',
type: 'text', type: 'text',
value: '', value: '',
validations: {
required: true,
regex: new RegExp('(^$)|((https?://.*)(d*)/?(.*))'),
},
}, },
headers: { headers: {
styleName: 'col-12', styleName: 'col-12',
label: 'Headers', label: 'Headers',
type: 'headers', type: 'headers',
value: [{ key: '', value: '' }], value: [{ key: '', value: '' }],
validations: {},
}, },
events: { events: {
styleName: 'col-12', styleName: 'col-12',
label: 'Hooks', label: 'Hooks',
type: 'events', type: 'events',
value: [], 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,25 +1,19 @@
import * as yup from 'yup'; import * as yup from 'yup';
import { translatedErrors } from 'strapi-helper-plugin'; import { translatedErrors } from 'strapi-helper-plugin';
import { NAME_REGEX, URL_REGEX } from './fieldsRegex';
const createYupSchema = form => const schema = yup.object().shape({
yup.object().shape( name: yup
Object.keys(form).reduce((acc, current) => { .string(translatedErrors.string)
const { type, validations } = form[current]; .nullable()
acc[current] = createYupSchemaEntry(type, validations); .required(translatedErrors.required)
.matches(NAME_REGEX, translatedErrors.regex),
return acc; url: yup
}, {}) .string(translatedErrors.string)
); .nullable()
.required(translatedErrors.required)
const createYupSchemaEntry = (type, validations) => { .matches(URL_REGEX, translatedErrors.regex),
let schema = yup.mixed(); headers: yup
if (['text'].includes(type)) {
schema = yup.string(translatedErrors.string).nullable();
}
if (['headers'].includes(type)) {
schema = yup
.array() .array()
.of( .of(
yup.object().shape({ yup.object().shape({
@ -27,28 +21,11 @@ const createYupSchemaEntry = (type, validations) => {
value: yup.string().required(), value: yup.string().required(),
}) })
) )
.nullable(); .nullable(),
} events: yup
.array()
if (['events'].includes(type)) { .nullable()
schema = yup.array(); .required(translatedErrors.required),
}
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 schema;
};
export default createYupSchema;

View File

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

View File

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

View File

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

View File

@ -1,26 +1 @@
<?xml version="1.0" encoding="UTF-8"?> <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>
<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>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 648 B